Compare commits

...

139 Commits

Author SHA1 Message Date
841c69af59 Merge pull request #348 from mailpoet/page_reviews
Page reviews
2016-02-12 13:36:47 -05:00
56b4688f93 updated unit tests for segments 2016-02-12 19:29:30 +01:00
e60bc7c387 handle duplicates in model 2016-02-12 19:24:04 +01:00
6094a83f4b Merge pull request #350 from mailpoet/rendering_engine_image_update
Updates logic behind image dimensions based on column width
2016-02-12 19:14:49 +02:00
91ddb98f56 Merge pull request #349 from mailpoet/exception_namescape_fix
Fixes namespace issue when catching exceptions
2016-02-12 19:11:25 +02:00
27d5972306 - Updates logic behind image dimensions based on column width 2016-02-12 12:05:27 -05:00
6088497433 Bump up release version to 0.0.15 2016-02-12 18:25:12 +02:00
0d894a6fef - Fixes namespace issue when catching exceptions 2016-02-12 11:20:43 -05:00
57f0b88299 Merge pull request #347 from mailpoet/sending_frequency
Implements sending frequency
2016-02-12 14:23:22 +02:00
5121dbe0c8 Form Editor Round 3
- added prefix to form styles so that it does not conflict when multiple forms are on the same page
- added validation on form save for segments
2016-02-12 12:45:07 +01:00
f874ffc19c Merge pull request #346 from mailpoet/rendering_engine_image_update
Updates image rendering & unit test
2016-02-12 12:42:49 +02:00
e928a5c2bc Segments page review
- remove edit link for WordPress users list
- hide trashed segments from Import
- fixed display issue in listing's item actions
2016-02-12 11:30:08 +01:00
d11badf3ce - Implements sending frequency
- Updates sending queue worker
2016-02-11 22:46:45 -05:00
3006c982cb - Updates image rendering & unit test 2016-02-11 21:28:43 -05:00
b29e31fdd6 Merge pull request #344 from mailpoet/router_update_sending_queue
Sending queue router update
2016-02-11 11:31:49 -05:00
bc42c8e280 Merge branch 'router_update_sending_queue' of mailpoet:mailpoet/mailpoet into router_update_sending_queue 2016-02-11 11:30:08 -05:00
409697ee64 Sending queue router update
- cleaned up useless code
- bugfixes
- improved code coverage
2016-02-11 11:30:01 -05:00
cfb4265971 Merge pull request #343 from mailpoet/queue_worker_rewrite
Sending queue worker rewrite
2016-02-11 18:16:07 +02:00
13d78aac05 Merge branch 'queue_worker_rewrite' of mailpoet:mailpoet/mailpoet into queue_worker_rewrite 2016-02-11 10:37:26 -05:00
6f176f4e6c - Updates MailChimp unit test 2016-02-11 10:34:24 -05:00
9b584296a5 - Updates queue worker based on code review comments 2016-02-11 10:23:41 -05:00
5ea87c5eed Sending queue router update
- cleaned up useless code
- bugfixes
- improved code coverage
2016-02-11 09:37:53 +01:00
7522084ccb - Rewrites sending queue worker and updates router
- Implements batch sending for queue worker
- Fixes mailer class issue when sender data can be empty
- Updates values for cron execution timeout/limit
2016-02-10 22:34:54 -05:00
214aa60d0e Merge pull request #338 from mailpoet/editor_polishing_2
Change `padded` image attribute to `fullWidth`
2016-02-10 22:34:36 -05:00
7a049ce1b7 - Rewrites sending queue worker and updates router
- Implements batch sending for queue worker
- Fixes mailer class issue when sender data can be empty
- Updates values for cron execution timeout/limit
2016-02-10 22:32:39 -05:00
94d293deb7 Merge pull request #339 from mailpoet/mailchimp_update
Updates MailChimp class and unit test
2016-02-09 18:04:43 +01:00
bd2d38d757 updated MP.Notice in order to handle arrays as error messages 2016-02-09 17:58:40 +01:00
abec524daa Merge pull request #340 from mailpoet/fix_registration_in_comments
Subscribe in comments
2016-02-09 18:41:58 +02:00
cac6beb4ac - Fixes display of error messages 2016-02-09 11:26:00 -05:00
cac995e15b fixed subscribe in comments 2016-02-09 16:55:00 +01:00
b26380fd10 - Updates MailChimp class and unit test 2016-02-09 10:03:29 -05:00
6c6a4070be Remove obsolete attribute from server response 2016-02-09 15:47:56 +02:00
8b001d820b Change padded image attribute to fullWidth 2016-02-09 15:26:06 +02:00
2ae3d8ebdf Merge pull request #337 from mailpoet/fix_listing_pagination
Pagination issues
2016-02-09 12:23:39 +02:00
459ec21f9d fixed pagination issues 2016-02-08 17:11:11 +01:00
9c7790d07e Merge pull request #336 from mailpoet/listing_empty_trash
Listing update + Unit tests
2016-02-08 16:01:42 +02:00
f9c5b99e46 updated Subscriber:: -> self:: in Models\Subscriber 2016-02-08 14:43:59 +01:00
95b0b39366 Fixed bulk_action success messages
- uptaded Subscriber & Segment routers' test
- moved add/remove segments logic to Subscriber::createOrUpdate
- fixed Router\Segments save not returning errors
2016-02-08 13:36:35 +01:00
4b6fa0e760 Added "Subscribers without a segment" filter
- removed useless dependency in filters.jsx
- fixed issue with Router\Subscribers::save() not updating segments
2016-02-08 10:32:06 +01:00
67a3440ced updated router unit tests - refactor + test bulk delete 2016-02-08 10:10:43 +01:00
0de372344a fixed bulkDelete 2016-02-06 15:17:19 +01:00
7a04eeb650 Listing: Empty trash button 2016-02-06 15:15:07 +01:00
8dbfe82922 Bump version up to 0.0.14 2016-02-05 17:34:53 +02:00
7322f2151c Merge pull request #334 from mailpoet/unit_tests_update
Enables conditional bypassing of unit tests
2016-02-05 16:38:31 +02:00
c43d2f240d - Updates MailChimp test and .env.sample 2016-02-05 09:35:32 -05:00
bbcd267b6f - Updates .env.sample with available options for unit tests 2016-02-05 09:00:28 -05:00
bbc4acb2a4 Merge pull request #335 from mailpoet/mailer_class_Fix
Fixes detection of reply_to address
2016-02-05 12:52:39 +02:00
c89cc5a919 Merge pull request #333 from mailpoet/test_email_fix
Test email fix
2016-02-05 12:19:40 +02:00
33075940de - Fixes detection of reply_to address 2016-02-04 19:22:11 -05:00
51c09b8360 Merge pull request #325 from mailpoet/router_unit_tests
Unit tests (Router\NewsletterTemplates & Router\Segments)
2016-02-04 18:59:09 -05:00
2fbf85f371 - Catches exception returned by mailer class when sender is not configured 2016-02-04 18:57:07 -05:00
24cb614adb - Enables conditional bypassing of unit tests 2016-02-04 18:41:18 -05:00
55f851208b Major update of unit tests / updated routers + models + react 2016-02-04 19:04:52 +01:00
990dac7727 Merge pull request #332 from mailpoet/editor_fixes
Editor bug fixes
2016-02-04 09:02:21 -05:00
233020ca20 fix unit tests 2016-02-04 14:38:25 +01:00
0c419cde16 Unit tests (Router\NewsletterTemplates & Router\Segments
- moved json_decode(body) inside NewsletterTemplate->asArray (override)
- updated NewsletterTemplate router accordingly
- finished Segments unit test
2016-02-04 14:36:08 +01:00
35f9530d8e Merge pull request #326 from mailpoet/router_unit_tests_2
More Unit Tests + Initializer fix
2016-02-04 08:24:15 -05:00
992fe2a6e9 Fix preventing dragging by settings/delete block tools 2016-02-04 13:47:46 +02:00
c2cb88f995 Unit tests fixed + models & routers update 2016-02-04 11:41:05 +01:00
ba69d659ab Changed editor to not send 'last_modified', let server pick one 2016-02-03 18:21:30 +02:00
397d988eb1 Allow block dragging only with "Move" tool, but not with others 2016-02-03 18:21:30 +02:00
d85f2341ec Change Posts/ALC to use MailPoet specific image size 2016-02-03 18:21:30 +02:00
2b3c288b5f Merge pull request #330 from mailpoet/template_footer_fix
Removes footer text/link color overrides for coffee shop template
2016-02-03 11:19:43 -05:00
8ec28a23a7 Removes footer text/link color overrides for coffee shop template 2016-02-03 15:04:18 +02:00
12c9623e2f Better Error handling for models
- added (array)getErrors() to models, returns false if no errors
- converted Forms::saveEditor method to use getErrors
- added error handling on the form editor view
2016-02-03 12:23:42 +01:00
8ba9fdccbc fix for initializer - widget is now registered 2016-02-03 10:42:56 +01:00
a2ef62302f More Unit Tests + Initializer fix
- added unit test for Router\Forms
- updated unit test for Model\Segment to reflect changes
2016-02-02 17:22:11 +01:00
24ecc879d3 Merge pull request #319 from mailpoet/php53-fix
Updates code to work with PHP 5.3
2016-02-02 13:25:23 +01:00
a1104a7f90 Merge pull request #320 from mailpoet/sending_queue_worker_fix
Sending queue worker fix
2016-02-01 16:32:42 +02:00
aa959810e9 Merge pull request #322 from mailpoet/code_coverage
Code coverage report
2016-02-01 16:31:53 +02:00
45b7a79277 Merge pull request #321 from mailpoet/router_upgrade_3
Router upgrade 3
2016-02-01 15:41:13 +02:00
41c8c0dae5 Code coverage report 2016-02-01 14:07:54 +01:00
2aeab7aaff Router Upgrade #3 & bugfix on Cron/Supervisor
- ALC
- Cron
- ImportExport
- Mailer
- Newsletters (only get method for consistency with other router get methods)
- Permissions
2016-02-01 13:00:11 +01:00
4fd0c4b484 Router updates + unit tests + React
- added -f flag to run unit test command in order to fail fast
- pass only id to "$endpoint->get($id)" in React forms instead of array
- updated routers according to the ->get($id) change
- refactored a bit the way form creation works
- added unit tests for Segments router
2016-02-01 11:56:21 +01:00
d4623cf763 Router update for Settings and Setup + unit tests 2016-02-01 11:56:21 +01:00
181c4fed08 - Fixes an issues with the sending queue worker throwing and error when
newsletter is not found
2016-01-31 21:35:34 -05:00
7884dd8389 - Updates code to work with PHP 5.3. Closes #307 2016-01-31 14:02:57 -05:00
b577d33414 Merge pull request #318 from mailpoet/minor_cron_update
Cron update
2016-01-29 22:03:55 +02:00
70de0a01bf - Moves cron timeout/execution limit to the central cron helper class 2016-01-29 15:01:10 -05:00
3b7f77d9af Bump up version to 0.0.13 2016-01-29 21:47:56 +02:00
21847ca875 Merge pull request #315 from mailpoet/sending_queue_worker_fix
Sending worker newsletter processing fix
2016-01-29 21:07:17 +02:00
6153316047 Merge pull request #314 from mailpoet/cron_loop_fix
Cron refactoring
2016-01-29 21:06:35 +02:00
32f8f07602 - Refactors and fixes issues identified during code review 2016-01-29 13:30:13 -05:00
70a04d9bf6 Merge pull request #317 from mailpoet/router_upgrade_2
CustomFields (Router update + Form block renaming + Unit tests)
2016-01-29 18:45:29 +02:00
bb1cc997cc CustomFields
- renamed form block type "input" to "text" for consistency with React
- updated CustomFields router to comply with main router
- unit tests for CF router
2016-01-29 17:16:24 +01:00
24f96d9d7d Merge pull request #313 from mailpoet/router_upgrade
Main Router update + Subscribers router update + Unit test
2016-01-29 17:01:21 +02:00
46c7332da2 Merge pull request #316 from mailpoet/sendgrid_mailer_fix
SendGrid mailer update
2016-01-29 12:53:44 +02:00
2f42f643ab - Fixes message body construction 2016-01-28 22:29:47 -05:00
63c87f3746 - Fixes issue with the sending worker failing to process newsletters 2016-01-28 21:40:30 -05:00
d4d575cda4 - Refactors cron supervisor/daemon/router 2016-01-28 21:38:23 -05:00
2cf03ec0a3 - Fixes cron HTTP request loop issue 2016-01-28 12:50:12 -05:00
72ad98a77f unit test for router Subscribers 2016-01-28 18:00:55 +01:00
b5094f568c Merge pull request #312 from mailpoet/mailpoet_bridge_update
MailPoet mailer update
2016-01-28 16:50:47 +02:00
7d224274fc - Rebases master & fixes some code 2016-01-28 09:40:57 -05:00
f3b9f7be92 - Updates MailPoet mailer to support batch sending
- Fixes message encoding issue
2016-01-28 09:40:57 -05:00
6e74f82ace Merge pull request #311 from mailpoet/mailer_tests_update
Disables send method for all mailer tests
2016-01-28 14:20:56 +02:00
831eb6af44 Merge pull request #310 from mailpoet/fix_install
Fix init/hooks so that we properly initialize db/plugin
2016-01-28 13:51:22 +02:00
1e8e5aecee Remove space after if 2016-01-28 13:50:58 +02:00
c0f98c9ba6 - Moves credentials from mailer tests to ENV file
- Disables send method by default unless enabled in ENV file
2016-01-27 18:36:38 -05:00
746c19d6ed - Resolves an issue with cron not starting 2016-01-27 14:15:00 -05:00
894a9e8c90 Router\Router is now taking care of outputting JSON
- converted Subscribers Router to return array instead of wp_send_json()
- added unit test for Subscribers Router
2016-01-27 17:23:30 +01:00
8fea917337 Merge pull request #308 from mailpoet/mailer_updates
Update to mailer methods
2016-01-27 14:13:37 +02:00
c60425afb2 Fix init/hooks so that we properly initialize db/plugin
- Added activation hook in main file (mailpoet.php)
- auto deactivate plugin in case of fatal errors during init
2016-01-27 12:52:40 +01:00
0776e9ad73 - Adds "reply to" option to all mailers
- Replaces WPMail with Swift using local transport (PHP mail)
- Fixes AmazonSES region naming convention
- Updates tests
2016-01-26 19:08:02 -05:00
91981cc324 Merge pull request #306 from mailpoet/mailer_cleanup
Removes Mandrill API mailer method
2016-01-26 18:52:10 +02:00
1906fafacb Merge pull request #301 from mailpoet/shortcodes_implementation
Shortcodes implementation
2016-01-26 18:32:15 +02:00
c11d95b402 - Refactors code 2016-01-26 11:29:56 -05:00
b4c8fe6f45 - Updates newsletter router and sending queue worker to work with the
shortcodes implementation
2016-01-26 10:44:18 -05:00
d0e770e0fc - Removes Mandrill API mailer method 2016-01-26 09:18:48 -05:00
3d6d1a4282 - Disables shortcodes unit test 2016-01-26 09:14:12 -05:00
dc3b47db00 - Implements shortcodes
- Updates newsletter router/sending queue worker to use shortcodes
  replacement class
2016-01-26 09:13:29 -05:00
900d6694e2 Merge pull request #302 from mailpoet/parsley_issue
removed parsley validation on step 3
2016-01-26 15:50:31 +02:00
e8074a61a5 Merge pull request #298 from mailpoet/text_block_rendering_fix
Text block formatting fix
2016-01-26 12:51:07 +02:00
64501a914a removed parsley validation on step 3
- fixed placeholder for select2 instances in settings
- fixed issue in sending queue worker when newsletter does not exist
2016-01-26 11:36:20 +01:00
de70e855ad - Replaces regex with DOM queries 2016-01-25 21:09:16 -05:00
03e3b5a94b - Fixes regex that formats heading tags 2016-01-25 21:08:16 -05:00
3331bed31c Merge pull request #300 from mailpoet/text_rendering
Text rendering
2016-01-25 18:16:16 +02:00
8c5a33a0fe Merge pull request #297 from mailpoet/alc_rendering
Automated latest content rendering
2016-01-25 17:35:51 +02:00
01eb6c7a98 - Refactors ALC 2016-01-25 09:27:51 -05:00
f502e0b677 - Implements text rendering
- Updates tests
- Updates newsletter router and sending queue worker to reflect changes to
  the renderer
2016-01-22 22:07:02 -05:00
a6b64a1c5d - Implements automated latest content rendering 2016-01-22 19:59:50 -05:00
5019131b21 Bump up version to 0.0.12 2016-01-22 16:44:09 +02:00
da483fb88f Merge pull request #296 from mailpoet/form_custom_fields
Form custom fields
2016-01-22 14:56:16 +02:00
788bed4622 moved const definition inside render method 2016-01-22 13:45:52 +01:00
3fbe5423d0 added constant for years range + added comment for month quirk 2016-01-22 13:38:43 +01:00
8357295be2 Merge pull request #295 from mailpoet/import_review_fixes
Various fixes based on Rafael's import review comments
2016-01-22 12:28:52 +02:00
8072b162d4 Unit tests for new methods in model subscriber 2016-01-22 11:28:26 +01:00
3f2f0ec1a9 Merge pull request #294 from mailpoet/mailchimp_import_fix
Fixes MailChimp import error
2016-01-22 12:20:44 +02:00
5a5a777b7d Added check for when the custom field doesn't exist
- suppressed cron supervisor error
2016-01-22 11:02:48 +01:00
6cac7f3652 Saving of date custom fields in React & PHP 2016-01-22 10:45:33 +01:00
6a9313107c - Fixes unit test 2016-01-21 12:21:22 -05:00
72c9d301b7 - Fixes issue with file extension warning
- Removes duplicate notices
- Updates Twig's localize function to escape double quotes
2016-01-21 12:08:51 -05:00
ad925de801 Custom fields (in Form & Edit subscriber) 2016-01-21 17:27:34 +01:00
1da28b7299 - Enforces CSV file extension during import file selection
- Updates "no records found" error message
2016-01-20 15:45:56 -05:00
e837ad7014 - Fixes MailChimp import error 2016-01-20 13:54:20 -05:00
daec56191f Save custom fields on subscribe
- added methods to get/set a specific custom field
- added method to get all custom fields (and assign each custom field to the subscriber's instance)
- fixed zIndex of form editor's toolbar (footer was positioned above, preventing click)
2016-01-19 17:02:05 +01:00
7bd25660df Merge pull request #293 from mailpoet/form_editor
Form editor update
2016-01-19 13:22:21 +02:00
3b9821fbe1 Remove matching form block when custom field is deleted 2016-01-18 17:46:42 +01:00
cabe2d61b7 Form editor update
- when a custom field is updated, the matching form field is now also updated
- ability to toggle "is_required" on First name & Last name
- fixed position of validation errors on segment selection, checkbox and radio
- fixed form subscription not working when using custom fields
- fixed sortable in segment selection after list update (add/remove)
- updated position of messages in form subscription
2016-01-18 17:23:10 +01:00
182 changed files with 5066 additions and 3080 deletions

View File

@ -1,2 +1,16 @@
WP_TEST_PATH="/var/www/wordpress" WP_TEST_PATH="/var/www/wordpress"
WP_TEST_ENABLE_NETWORK_TESTS="true"
WP_TEST_IMPORT_MAILCHIMP_API=""
WP_TEST_IMPORT_MAILCHIMP_LISTS="" // (separated with comma)
WP_TEST_MAILER_ENABLE_SENDING="true"
WP_TEST_MAILER_AMAZON_ACCESS=""
WP_TEST_MAILER_AMAZON_SECRET=""
WP_TEST_MAILER_AMAZON_REGION=""
WP_TEST_MAILER_ELASTICEMAIL_API=""
WP_TEST_MAILER_MAILGUN_API=""
WP_TEST_MAILER_MAILGUN_DOMAIN=""
WP_TEST_MAILER_MAILPOET_API=""
WP_TEST_MAILER_SENDGRID_API=""
WP_TEST_MAILER_SMTP_HOST=""
WP_TEST_MAILER_SMTP_LOGIN=""
WP_TEST_MAILER_SMTP_PASSWORD=""

View File

@ -105,7 +105,18 @@ class RoboFile extends \Robo\Tasks {
function testUnit($file = null) { function testUnit($file = null) {
$this->loadEnv(); $this->loadEnv();
$this->_exec('vendor/bin/codecept build'); $this->_exec('vendor/bin/codecept build');
$this->_exec('vendor/bin/codecept run unit '.(($file) ? $file : '')); $this->_exec('vendor/bin/codecept run unit -f '.(($file) ? $file : ''));
}
function testCoverage($file = null) {
$this->loadEnv();
$this->_exec('vendor/bin/codecept build');
$this->_exec(join(' ', array(
'vendor/bin/codecept run',
(($file) ? $file : ''),
'--coverage',
'--coverage-html'
)));
} }
function testJavascript() { function testJavascript() {

View File

@ -125,6 +125,7 @@ handle_icon = '../img/handle.png'
float: none float: none
#mailpoet_form_toolbar #mailpoet_form_toolbar
z-index: 999
position: absolute position: absolute
width: 400px width: 400px

View File

@ -1,31 +1,31 @@
define([ define([
'react', 'react'
'react-checkbox-group'
], ],
function( function(
React, React
CheckboxGroup
) { ) {
var FormFieldCheckbox = React.createClass({ const FormFieldCheckbox = React.createClass({
onValueChange: function(e) {
e.target.value = this.refs.checkbox.checked ? '1' : '';
return this.props.onValueChange(e);
},
render: function() { render: function() {
var selected_values = this.props.item[this.props.field.name] || ''; const isChecked = !!(this.props.item[this.props.field.name]);
if(
selected_values !== undefined
&& selected_values.constructor !== Array
) {
selected_values = selected_values.split(';').map(function(value) {
return value.trim();
});
}
var count = Object.keys(this.props.field.values).length;
var options = Object.keys(this.props.field.values).map( const options = Object.keys(this.props.field.values).map(
function(value, index) { function(value, index) {
return ( return (
<p key={ 'checkbox-' + index }> <p key={ 'checkbox-' + index }>
<label> <label>
<input type="checkbox" value={ value } /> <input
&nbsp;{ this.props.field.values[value] } ref="checkbox"
type="checkbox"
value="1"
checked={ isChecked }
onChange={ this.onValueChange }
name={ this.props.field.name }
/>
{ this.props.field.values[value] }
</label> </label>
</p> </p>
); );
@ -33,30 +33,10 @@ function(
); );
return ( return (
<CheckboxGroup <div>
name={ this.props.field.name }
value={ selected_values }
ref={ this.props.field.name }
onChange={ this.handleValueChange }>
{ options } { options }
</CheckboxGroup> </div>
); );
},
handleValueChange: function() {
var field = this.props.field.name;
var group = this.refs[field];
var selected_values = [];
if(group !== undefined) {
selected_values = group.getCheckedValues();
}
return this.props.onValueChange({
target: {
name: field,
value: selected_values.join(';')
}
});
} }
}); });

View File

@ -0,0 +1,196 @@
define([
'react',
'moment',
], function(
React,
Moment
) {
class FormFieldDateYear extends React.Component {
render() {
const yearsRange = 100;
const years = [];
const currentYear = Moment().year();
for (let i = currentYear; i >= currentYear - yearsRange; i--) {
years.push((
<option
key={ i }
value={ i }
>{ i }</option>
));
}
return (
<select
name={ this.props.name + '[year]' }
value={ this.props.year }
onChange={ this.props.onValueChange }
>
{ years }
</select>
);
}
}
class FormFieldDateMonth extends React.Component {
render() {
const months = [];
for (let i = 1; i <= 12; i++) {
months.push((
<option
key={ i }
value={ i }
>{ this.props.monthNames[i - 1] }</option>
));
}
return (
<select
name={ this.props.name + '[month]' }
value={ this.props.month }
onChange={ this.props.onValueChange }
>
{ months }
</select>
);
}
}
class FormFieldDateDay extends React.Component {
render() {
const days = [];
for (let i = 1; i <= 31; i++) {
days.push((
<option
key={ i }
value={ i }
>{ i }</option>
));
}
return (
<select
name={ this.props.name + '[day]' }
value={ this.props.day }
onChange={ this.props.onValueChange }
>
{ days }
</select>
);
}
}
class FormFieldDate extends React.Component {
constructor(props) {
super(props);
this.state = {
year: Moment().year(),
month: 1,
day: 1
}
}
componentDidMount() {
}
componentDidUpdate(prevProps, prevState) {
if (
(this.props.item !== undefined && prevProps.item !== undefined)
&& (this.props.item.id !== prevProps.item.id)
) {
this.extractTimeStamp();
}
}
extractTimeStamp() {
const timeStamp = parseInt(this.props.item[this.props.field.name], 10);
this.setState({
year: Moment.unix(timeStamp).year(),
// Moment returns the month as [0..11]
// We increment it to match PHP's mktime() which expects [1..12]
month: Moment.unix(timeStamp).month() + 1,
day: Moment.unix(timeStamp).date()
});
}
updateTimeStamp(field) {
let newTimeStamp = Moment(
`${this.state.month}/${this.state.day}/${this.state.year}`,
'M/D/YYYY'
).valueOf();
if (!isNaN(newTimeStamp) && parseInt(newTimeStamp, 10) > 0) {
// convert milliseconds to seconds
newTimeStamp /= 1000;
return this.props.onValueChange({
target: {
name: field,
value: newTimeStamp
}
});
}
}
onValueChange(e) {
// extract property from name
const matches = e.target.name.match(/(.*?)\[(.*?)\]/);
let field = null;
let property = null;
if (matches !== null && matches.length === 3) {
field = matches[1];
property = matches[2];
let value = parseInt(e.target.value, 10);
this.setState({
[`${property}`]: value
}, () => {
this.updateTimeStamp(field);
});
}
}
render() {
const monthNames = window.mailpoet_month_names || [];
const dateType = this.props.field.params.date_type;
const dateSelects = dateType.split('_');
const fields = dateSelects.map(type => {
switch(type) {
case 'year':
return (<FormFieldDateYear
onValueChange={ this.onValueChange.bind(this) }
ref={ 'year' }
key={ 'year' }
name={ this.props.field.name }
year={ this.state.year }
/>);
break;
case 'month':
return (<FormFieldDateMonth
onValueChange={ this.onValueChange.bind(this) }
ref={ 'month' }
key={ 'month' }
name={ this.props.field.name }
month={ this.state.month }
monthNames={ monthNames }
/>);
break;
case 'day':
return (<FormFieldDateDay
onValueChange={ this.onValueChange.bind(this) }
ref={ 'day' }
key={ 'day' }
name={ this.props.field.name }
day={ this.state.day }
/>);
break;
}
});
return (
<div>
{fields}
</div>
);
}
};
return FormFieldDate;
});

View File

@ -5,7 +5,8 @@ define([
'form/fields/select.jsx', 'form/fields/select.jsx',
'form/fields/radio.jsx', 'form/fields/radio.jsx',
'form/fields/checkbox.jsx', 'form/fields/checkbox.jsx',
'form/fields/selection.jsx' 'form/fields/selection.jsx',
'form/fields/date.jsx',
], ],
function( function(
React, React,
@ -14,7 +15,8 @@ function(
FormFieldSelect, FormFieldSelect,
FormFieldRadio, FormFieldRadio,
FormFieldCheckbox, FormFieldCheckbox,
FormFieldSelection FormFieldSelection,
FormFieldDate
) { ) {
var FormField = React.createClass({ var FormField = React.createClass({
renderField: function(data, inline = false) { renderField: function(data, inline = false) {
@ -55,6 +57,10 @@ function(
case 'selection': case 'selection':
field = (<FormFieldSelection {...data} />); field = (<FormFieldSelection {...data} />);
break; break;
case 'date':
field = (<FormFieldDate {...data} />);
break;
} }
if(inline === true) { if(inline === true) {
@ -66,10 +72,10 @@ function(
); );
} else { } else {
return ( return (
<p key={ 'field-' + (data.index || 0) }> <div key={ 'field-' + (data.index || 0) }>
{ field } { field }
{ description } { description }
</p> </div>
); );
} }
}, },

View File

@ -7,7 +7,6 @@ function(
var FormFieldRadio = React.createClass({ var FormFieldRadio = React.createClass({
render: function() { render: function() {
var selected_value = this.props.item[this.props.field.name]; var selected_value = this.props.item[this.props.field.name];
var count = Object.keys(this.props.field.values).length;
var options = Object.keys(this.props.field.values).map( var options = Object.keys(this.props.field.values).map(
function(value, index) { function(value, index) {
@ -20,7 +19,7 @@ function(
value={ value } value={ value }
onChange={ this.props.onValueChange } onChange={ this.props.onValueChange }
name={ this.props.field.name } /> name={ this.props.field.name } />
&nbsp;{ this.props.field.values[value] } { this.props.field.values[value] }
</label> </label>
</p> </p>
); );

View File

@ -120,7 +120,7 @@ function(
<select <select
id={ this.props.field.id || this.props.field.name } id={ this.props.field.id || this.props.field.name }
ref="select" ref="select"
placeholder={ this.props.field.placeholder } data-placeholder={ this.props.field.placeholder }
multiple={ this.props.field.multiple } multiple={ this.props.field.multiple }
defaultValue={ default_value } defaultValue={ default_value }
{...this.props.field.validation} {...this.props.field.validation}

View File

@ -48,7 +48,7 @@ define(
MailPoet.Ajax.post({ MailPoet.Ajax.post({
endpoint: this.props.endpoint, endpoint: this.props.endpoint,
action: 'get', action: 'get',
data: { id: id } data: id
}).done(function(response) { }).done(function(response) {
if(response === false) { if(response === false) {
this.setState({ this.setState({

View File

@ -617,6 +617,28 @@ var WysijaForm = {
// this is a url, so do not encode the protocol // this is a url, so do not encode the protocol
return encodeURI(str).replace(/[!'()*]/g, escape); return encodeURI(str).replace(/[!'()*]/g, escape);
} }
},
updateBlock: function(field) {
var hasUpdated = false;
WysijaForm.getBlocks().each(function(b) {
if(b.block.getData().id === field.id) {
hasUpdated = true;
b.block.redraw(field);
}
});
return hasUpdated;
},
removeBlock: function(field, callback) {
var hasRemoved = false;
WysijaForm.getBlocks().each(function(b) {
if(b.block.getData().id === field.id) {
hasRemoved = true;
b.block.removeBlock(callback);
}
});
return hasRemoved;
} }
}; };
@ -825,10 +847,6 @@ WysijaForm.Block = Class.create({
Effect.Fade(this.element.identify(), { Effect.Fade(this.element.identify(), {
duration: 0.2, duration: 0.2,
afterFinish: function(effect) { afterFinish: function(effect) {
if(effect.element.next('.mailpoet_form_block') !== undefined && callback !== false) {
// show controls of next block to allow mass delete
WysijaForm.get(effect.element.next('.mailpoet_form_block')).block.showControls();
}
// remove placeholder // remove placeholder
if(effect.element.previous('.block_placeholder') !== undefined) { if(effect.element.previous('.block_placeholder') !== undefined) {
effect.element.previous('.block_placeholder').remove(); effect.element.previous('.block_placeholder').remove();

View File

@ -25,58 +25,49 @@ const columns = [
const messages = { const messages = {
onTrash: function(response) { onTrash: function(response) {
if(response) { var count = ~~response;
let message = null; var message = null;
if(~~response === 1) {
message = (
'1 form was moved to the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d forms were moved to the trash.'
).replace('%$1d', ~~response);
}
if(message !== null) { if(count === 1) {
MailPoet.Notice.success(message); message = (
} '1 form was moved to the trash.'
);
} else {
message = (
'%$1d forms were moved to the trash.'
).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
}, },
onDelete: function(response) { onDelete: function(response) {
if(response) { var count = ~~response;
let message = null; var message = null;
if(~~response === 1) {
message = (
'1 form was permanently deleted.'
);
} else if(~~response > 1) {
message = (
'%$1d forms were permanently deleted.'
).replace('%$1d', ~~response);
}
if(message !== null) { if(count === 1) {
MailPoet.Notice.success(message); message = (
} '1 form was permanently deleted.'
);
} else {
message = (
'%$1d forms were permanently deleted.'
).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
}, },
onRestore: function(response) { onRestore: function(response) {
if(response) { var count = ~~response;
let message = null; var message = null;
if(~~response === 1) {
message = (
'1 form has been restored from the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d forms have been restored from the trash.'
).replace('%$1d', ~~response);
}
if(message !== null) { if(count === 1) {
MailPoet.Notice.success(message); message = (
} '1 form has been restored from the trash.'
);
} else {
message = (
'%$1d forms have been restored from the trash.'
).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
} }
}; };
@ -125,8 +116,8 @@ const FormList = React.createClass({
endpoint: 'forms', endpoint: 'forms',
action: 'create' action: 'create'
}).done(function(response) { }).done(function(response) {
if(response !== false) { if(response.result && response.form_id) {
window.location = response; window.location = mailpoet_form_edit_url + response.form_id;
} }
}); });
}, },

View File

@ -14,6 +14,9 @@ function(
}) })
return this.props.onSelectFilter(filters); return this.props.onSelectFilter(filters);
}, },
handleEmptyTrash: function() {
return this.props.onEmptyTrash();
},
getAvailableFilters: function() { getAvailableFilters: function() {
let filters = this.props.filters; let filters = this.props.filters;
@ -34,7 +37,7 @@ function(
const available_filters = this.getAvailableFilters() const available_filters = this.getAvailableFilters()
.map(function(filter, i) { .map(function(filter, i) {
let default_value = false; let default_value = false;
if(selected_filters[filter] !== undefined && selected_filters[filter]) { if (selected_filters[filter] !== undefined && selected_filters[filter]) {
default_value = selected_filters[filter] default_value = selected_filters[filter]
} else { } else {
jQuery(`select[name="${filter}"]`).val(''); jQuery(`select[name="${filter}"]`).val('');
@ -60,9 +63,10 @@ function(
let button = false; let button = false;
if(available_filters.length > 0) { if (available_filters.length > 0) {
button = ( button = (
<input <input
id="post-query-submit"
onClick={ this.handleFilterAction } onClick={ this.handleFilterAction }
type="submit" type="submit"
defaultValue="Filter" defaultValue="Filter"
@ -70,10 +74,23 @@ function(
); );
} }
let empty_trash = false;
if (this.props.group === 'trash') {
empty_trash = (
<input
onClick={ this.handleEmptyTrash }
type="submit"
value="Empty Trash"
className="button"
/>
);
}
return ( return (
<div className="alignleft actions actions"> <div className="alignleft actions actions">
{ available_filters } { available_filters }
{ button } { button }
{ empty_trash }
</div> </div>
); );
} }

View File

@ -74,10 +74,11 @@ define(
); );
} }
var custom_actions = this.props.item_actions; const custom_actions = this.props.item_actions;
var item_actions = false; let item_actions = false;
if(custom_actions.length > 0) { if(custom_actions.length > 0) {
let is_first = true;
item_actions = custom_actions.map(function(action, index) { item_actions = custom_actions.map(function(action, index) {
if(action.display !== undefined) { if(action.display !== undefined) {
if(action.display(this.props.item) === false) { if(action.display(this.props.item) === false) {
@ -85,10 +86,12 @@ define(
} }
} }
let custom_action = null;
if(action.name === 'trash') { if(action.name === 'trash') {
return ( custom_action = (
<span key={ 'action-'+index } className="trash"> <span key={ 'action-'+index } className="trash">
{(index > 0) ? ' | ' : ''} {(!is_first) ? ' | ' : ''}
<a <a
href="javascript:;" href="javascript:;"
onClick={ this.handleTrashItem.bind( onClick={ this.handleTrashItem.bind(
@ -100,27 +103,27 @@ define(
</span> </span>
); );
} else if(action.refresh) { } else if(action.refresh) {
return ( custom_action = (
<span <span
onClick={ this.props.onRefreshItems } onClick={ this.props.onRefreshItems }
key={ 'action-'+index } className={ action.name }> key={ 'action-'+index } className={ action.name }>
{(index > 0) ? ' | ' : ''} {(!is_first) ? ' | ' : ''}
{ action.link(this.props.item) } { action.link(this.props.item) }
</span> </span>
); );
} else if(action.link) { } else if(action.link) {
return ( custom_action = (
<span <span
key={ 'action-'+index } className={ action.name }> key={ 'action-'+index } className={ action.name }>
{(index > 0) ? ' | ' : ''} {(!is_first) ? ' | ' : ''}
{ action.link(this.props.item) } { action.link(this.props.item) }
</span> </span>
); );
} else { } else {
return ( custom_action = (
<span <span
key={ 'action-'+index } className={ action.name }> key={ 'action-'+index } className={ action.name }>
{(index > 0) ? ' | ' : ''} {(!is_first) ? ' | ' : ''}
<a href="javascript:;" onClick={ <a href="javascript:;" onClick={
(action.onClick !== undefined) (action.onClick !== undefined)
? action.onClick.bind(null, ? action.onClick.bind(null,
@ -132,6 +135,12 @@ define(
</span> </span>
); );
} }
if(custom_action !== null && is_first === true) {
is_first = false;
}
return custom_action;
}.bind(this)); }.bind(this));
} else { } else {
item_actions = ( item_actions = (
@ -504,10 +513,21 @@ define(
this.getItems(); this.getItems();
}.bind(this)); }.bind(this));
}, },
handleEmptyTrash: function() {
this.handleBulkAction('all', {
action: 'delete',
group: 'trash'
}, function(response) {
MailPoet.Notice.success(
MailPoetI18n.permanentlyDeleted.replace('%d', response)
);
});
},
handleBulkAction: function(selected_ids, params, callback) { handleBulkAction: function(selected_ids, params, callback) {
if( if(
this.state.selection === false this.state.selection === false
&& this.state.selected_ids.length === 0 && this.state.selected_ids.length === 0
&& selected_ids !== 'all'
) { ) {
return; return;
} }
@ -520,8 +540,10 @@ define(
limit: 0, limit: 0,
filter: this.state.filter, filter: this.state.filter,
group: this.state.group, group: this.state.group,
search: this.state.search, search: this.state.search
selection: selected_ids }
if(selected_ids !== 'all') {
data.listing.selection = selected_ids;
} }
MailPoet.Ajax.post({ MailPoet.Ajax.post({
@ -715,7 +737,10 @@ define(
<ListingFilters <ListingFilters
filters={ this.state.filters } filters={ this.state.filters }
filter={ this.state.filter } filter={ this.state.filter }
onSelectFilter={ this.handleFilter } /> group={ this.state.group }
onSelectFilter={ this.handleFilter }
onEmptyTrash={ this.handleEmptyTrash }
/>
<ListingPages <ListingPages
count={ this.state.count } count={ this.state.count }
page={ this.state.page } page={ this.state.page }

View File

@ -7,7 +7,11 @@ define(['react', 'classnames'], function(React, classNames) {
} }
}, },
setPage: function(page) { setPage: function(page) {
this.props.onSetPage(page); this.setState({
page: null
}, function () {
this.props.onSetPage(this.constrainPage(page));
}.bind(this));
}, },
setFirstPage: function() { setFirstPage: function() {
this.setPage(1); this.setPage(1);
@ -16,10 +20,14 @@ define(['react', 'classnames'], function(React, classNames) {
this.setPage(this.getLastPage()); this.setPage(this.getLastPage());
}, },
setPreviousPage: function() { setPreviousPage: function() {
this.setPage(this.constrainPage(this.props.page - 1)); this.setPage(this.constrainPage(
parseInt(this.props.page, 10) - 1)
);
}, },
setNextPage: function() { setNextPage: function() {
this.setPage(this.constrainPage(this.props.page + 1)); this.setPage(this.constrainPage(
parseInt(this.props.page, 10) + 1)
);
}, },
constrainPage: function(page) { constrainPage: function(page) {
return Math.min(Math.max(1, Math.abs(~~page)), this.getLastPage()); return Math.min(Math.max(1, Math.abs(~~page)), this.getLastPage());
@ -27,14 +35,16 @@ define(['react', 'classnames'], function(React, classNames) {
handleSetManualPage: function(e) { handleSetManualPage: function(e) {
if(e.which === 13) { if(e.which === 13) {
this.setPage(this.state.page); this.setPage(this.state.page);
this.setState({ page: null });
} }
}, },
handleChangeManualPage: function(e) { handleChangeManualPage: function(e) {
this.setState({ this.setState({
page: this.constrainPage(e.target.value) page: e.target.value
}); });
}, },
handleBlurManualPage: function(e) {
this.setPage(e.target.value);
},
getLastPage: function() { getLastPage: function() {
return Math.ceil(this.props.count / this.props.limit); return Math.ceil(this.props.count / this.props.limit);
}, },
@ -101,6 +111,11 @@ define(['react', 'classnames'], function(React, classNames) {
); );
} }
let pageValue = this.props.page;
if(this.state.page !== null) {
pageValue = this.state.page;
}
pagination = ( pagination = (
<span className="pagination-links"> <span className="pagination-links">
{firstPage} {firstPage}
@ -115,10 +130,11 @@ define(['react', 'classnames'], function(React, classNames) {
type="text" type="text"
onChange={ this.handleChangeManualPage } onChange={ this.handleChangeManualPage }
onKeyUp={ this.handleSetManualPage } onKeyUp={ this.handleSetManualPage }
onBlur={ this.handleBlurManualPage }
aria-describedby="table-paging" aria-describedby="table-paging"
size="1" size="1"
ref="page" ref="page"
value={ this.state.page || this.props.page } value={ pageValue }
name="paged" name="paged"
id="current-page-selector" id="current-page-selector"
className="current-page" /> className="current-page" />

View File

@ -35,7 +35,7 @@ define([
titlePosition: 'inTextBlock', // 'inTextBlock'|'aboveBlock', titlePosition: 'inTextBlock', // 'inTextBlock'|'aboveBlock',
titleAlignment: 'left', // 'left'|'center'|'right' titleAlignment: 'left', // 'left'|'center'|'right'
titleIsLink: false, // false|true titleIsLink: false, // false|true
imagePadded: true, // true|false imageFullWidth: false, // true|false
//imageAlignment: 'centerPadded', // 'centerFull'|'centerPadded'|'left'|'right'|'alternate'|'none' //imageAlignment: 'centerPadded', // 'centerFull'|'centerPadded'|'left'|'right'|'alternate'|'none'
showAuthor: 'no', // 'no'|'aboveText'|'belowText' showAuthor: 'no', // 'no'|'aboveText'|'belowText'
authorPrecededBy: 'Author:', authorPrecededBy: 'Author:',
@ -63,7 +63,7 @@ define([
initialize: function() { initialize: function() {
base.BlockView.prototype.initialize.apply(this, arguments); base.BlockView.prototype.initialize.apply(this, arguments);
this.fetchPosts(); this.fetchPosts();
this.on('change:amount change:contentType change:terms change:inclusionType change:displayType change:titleFormat change:titlePosition change:titleAlignment change:titleIsLink change:imagePadded change:showAuthor change:authorPrecededBy change:showCategories change:categoriesPrecededBy change:readMoreType change:readMoreText change:sortBy change:showDivider', this._scheduleFetchPosts, this); this.on('change:amount change:contentType change:terms change:inclusionType change:displayType change:titleFormat change:titlePosition change:titleAlignment change:titleIsLink change:imageFullWidth change:showAuthor change:authorPrecededBy change:showCategories change:categoriesPrecededBy change:readMoreType change:readMoreText change:sortBy change:showDivider', this._scheduleFetchPosts, this);
this.listenTo(this.get('readMoreButton'), 'change', this._scheduleFetchPosts); this.listenTo(this.get('readMoreButton'), 'change', this._scheduleFetchPosts);
this.listenTo(this.get('divider'), 'change', this._scheduleFetchPosts); this.listenTo(this.get('divider'), 'change', this._scheduleFetchPosts);
}, },
@ -144,7 +144,7 @@ define([
"change .mailpoet_automated_latest_content_include_or_exclude": _.partial(this.changeField, "inclusionType"), "change .mailpoet_automated_latest_content_include_or_exclude": _.partial(this.changeField, "inclusionType"),
"change .mailpoet_automated_latest_content_title_position": _.partial(this.changeField, "titlePosition"), "change .mailpoet_automated_latest_content_title_position": _.partial(this.changeField, "titlePosition"),
"change .mailpoet_automated_latest_content_title_alignment": _.partial(this.changeField, "titleAlignment"), "change .mailpoet_automated_latest_content_title_alignment": _.partial(this.changeField, "titleAlignment"),
"change .mailpoet_automated_latest_content_image_padded": _.partial(this.changeBoolField, "imagePadded"), "change .mailpoet_automated_latest_content_image_full_width": _.partial(this.changeBoolField, "imageFullWidth"),
"change .mailpoet_automated_latest_content_show_author": _.partial(this.changeField, "showAuthor"), "change .mailpoet_automated_latest_content_show_author": _.partial(this.changeField, "showAuthor"),
"keyup .mailpoet_automated_latest_content_author_preceded_by": _.partial(this.changeField, "authorPrecededBy"), "keyup .mailpoet_automated_latest_content_author_preceded_by": _.partial(this.changeField, "authorPrecededBy"),
"change .mailpoet_automated_latest_content_show_categories": _.partial(this.changeField, "showCategories"), "change .mailpoet_automated_latest_content_show_categories": _.partial(this.changeField, "showCategories"),

View File

@ -20,7 +20,7 @@ define([
link: 'http://example.org', link: 'http://example.org',
src: 'no-image.png', src: 'no-image.png',
alt: 'An image of...', alt: 'An image of...',
padded: true, // true | false - Padded or full width fullWidth: true, // true | false
width: '64px', width: '64px',
height: '64px', height: '64px',
styles: { styles: {
@ -45,10 +45,10 @@ define([
this.toolsView = new Module.ImageBlockToolsView({ model: this.model }); this.toolsView = new Module.ImageBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView); this.toolsRegion.show(this.toolsView);
if (this.model.get('padded')) { if (this.model.get('fullWidth')) {
this.$el.removeClass('mailpoet_full_image');
} else {
this.$el.addClass('mailpoet_full_image'); this.$el.addClass('mailpoet_full_image');
} else {
this.$el.removeClass('mailpoet_full_image');
} }
}, },
}); });
@ -64,7 +64,7 @@ define([
"keyup .mailpoet_field_image_link": _.partial(this.changeField, "link"), "keyup .mailpoet_field_image_link": _.partial(this.changeField, "link"),
"keyup .mailpoet_field_image_address": _.partial(this.changeField, "src"), "keyup .mailpoet_field_image_address": _.partial(this.changeField, "src"),
"keyup .mailpoet_field_image_alt_text": _.partial(this.changeField, "alt"), "keyup .mailpoet_field_image_alt_text": _.partial(this.changeField, "alt"),
"change .mailpoet_field_image_padded": _.partial(this.changeBoolCheckboxField, "padded"), "change .mailpoet_field_image_full_width": _.partial(this.changeBoolCheckboxField, "fullWidth"),
"change .mailpoet_field_image_alignment": _.partial(this.changeField, "styles.block.textAlign"), "change .mailpoet_field_image_alignment": _.partial(this.changeField, "styles.block.textAlign"),
"click .mailpoet_field_image_select_another_image": "showMediaManager", "click .mailpoet_field_image_select_another_image": "showMediaManager",
"click .mailpoet_done_editing": "close", "click .mailpoet_done_editing": "close",

View File

@ -46,7 +46,7 @@ define([
titlePosition: 'inTextBlock', // 'inTextBlock'|'aboveBlock', titlePosition: 'inTextBlock', // 'inTextBlock'|'aboveBlock',
titleAlignment: 'left', // 'left'|'center'|'right' titleAlignment: 'left', // 'left'|'center'|'right'
titleIsLink: false, // false|true titleIsLink: false, // false|true
imagePadded: true, // true|false imageFullWidth: false, // true|false
//imageAlignment: 'centerPadded', // 'centerFull'|'centerPadded'|'left'|'right'|'alternate'|'none' //imageAlignment: 'centerPadded', // 'centerFull'|'centerPadded'|'left'|'right'|'alternate'|'none'
showAuthor: 'no', // 'no'|'aboveText'|'belowText' showAuthor: 'no', // 'no'|'aboveText'|'belowText'
authorPrecededBy: 'Author:', authorPrecededBy: 'Author:',
@ -88,7 +88,7 @@ define([
this.on('change:amount change:contentType change:terms change:inclusionType change:postStatus change:search change:sortBy', refreshAvailablePosts); this.on('change:amount change:contentType change:terms change:inclusionType change:postStatus change:search change:sortBy', refreshAvailablePosts);
this.listenTo(this.get('_selectedPosts'), 'add remove reset', refreshTransformedPosts); this.listenTo(this.get('_selectedPosts'), 'add remove reset', refreshTransformedPosts);
this.on('change:displayType change:titleFormat change:titlePosition change:titleAlignment change:titleIsLink change:imagePadded change:showAuthor change:authorPrecededBy change:showCategories change:categoriesPrecededBy change:readMoreType change:readMoreText change:showDivider', refreshTransformedPosts); this.on('change:displayType change:titleFormat change:titlePosition change:titleAlignment change:titleIsLink change:imageFullWidth change:showAuthor change:authorPrecededBy change:showCategories change:categoriesPrecededBy change:readMoreType change:readMoreText change:showDivider', refreshTransformedPosts);
this.listenTo(this.get('readMoreButton'), 'change', refreshTransformedPosts); this.listenTo(this.get('readMoreButton'), 'change', refreshTransformedPosts);
this.listenTo(this.get('divider'), 'change', refreshTransformedPosts); this.listenTo(this.get('divider'), 'change', refreshTransformedPosts);
@ -396,7 +396,7 @@ define([
"change .mailpoet_posts_include_or_exclude": _.partial(this.changeField, "inclusionType"), "change .mailpoet_posts_include_or_exclude": _.partial(this.changeField, "inclusionType"),
"change .mailpoet_posts_title_position": _.partial(this.changeField, "titlePosition"), "change .mailpoet_posts_title_position": _.partial(this.changeField, "titlePosition"),
"change .mailpoet_posts_title_alignment": _.partial(this.changeField, "titleAlignment"), "change .mailpoet_posts_title_alignment": _.partial(this.changeField, "titleAlignment"),
"change .mailpoet_posts_image_padded": _.partial(this.changeBoolField, "imagePadded"), "change .mailpoet_posts_image_full_width": _.partial(this.changeBoolField, "imageFullWidth"),
"change .mailpoet_posts_show_author": _.partial(this.changeField, "showAuthor"), "change .mailpoet_posts_show_author": _.partial(this.changeField, "showAuthor"),
"keyup .mailpoet_posts_author_preceded_by": _.partial(this.changeField, "authorPrecededBy"), "keyup .mailpoet_posts_author_preceded_by": _.partial(this.changeField, "authorPrecededBy"),
"change .mailpoet_posts_show_categories": _.partial(this.changeField, "showCategories"), "change .mailpoet_posts_show_categories": _.partial(this.changeField, "showCategories"),

View File

@ -9,7 +9,7 @@ define([
Module._query = function(args) { Module._query = function(args) {
return MailPoet.Ajax.post({ return MailPoet.Ajax.post({
endpoint: 'wordpress', endpoint: 'automatedLatestContent',
action: args.action, action: args.action,
data: args.options || {}, data: args.options || {},
}); });

View File

@ -11,7 +11,7 @@ define([
// Does not hold newsletter content nor newsletter styles, those are // Does not hold newsletter content nor newsletter styles, those are
// handled by other components. // handled by other components.
Module.NewsletterModel = SuperModel.extend({ Module.NewsletterModel = SuperModel.extend({
stale: ['body'], stale: ['body', 'created_at', 'deleted_at', 'updated_at'],
initialize: function(options) { initialize: function(options) {
this.on('change', function() { this.on('change', function() {
App.getChannel().trigger('autoSave'); App.getChannel().trigger('autoSave');

View File

@ -225,6 +225,11 @@ define([
showPreview: function() { showPreview: function() {
var json = App.toJSON(); var json = App.toJSON();
// Stringify to enable transmission of primitive non-string value types
if (!_.isUndefined(json.body)) {
json.body = JSON.stringify(json.body);
}
MailPoet.Ajax.post({ MailPoet.Ajax.post({
endpoint: 'newsletters', endpoint: 'newsletters',
action: 'render', action: 'render',

View File

@ -43,58 +43,49 @@ define(
var messages = { var messages = {
onTrash: function(response) { onTrash: function(response) {
var count = ~~response.newsletters; var count = ~~response;
var message = null; var message = null;
if(count === 1 || response === true) { if(count === 1) {
message = ( message = (
'1 newsletter was moved to the trash.' '1 newsletter was moved to the trash.'
); );
} else if(count > 1) { } else {
message = ( message = (
'%$1d newsletters were moved to the trash.' '%$1d newsletters were moved to the trash.'
).replace('%$1d', count); ).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
if(message !== null) {
MailPoet.Notice.success(message);
}
}, },
onDelete: function(response) { onDelete: function(response) {
var count = ~~response.newsletters; var count = ~~response;
var message = null; var message = null;
if(count === 1 || response === true) { if(count === 1) {
message = ( message = (
'1 newsletter was permanently deleted.' '1 newsletter was permanently deleted.'
); );
} else if(count > 1) { } else {
message = ( message = (
'%$1d newsletters were permanently deleted.' '%$1d newsletters were permanently deleted.'
).replace('%$1d', count); ).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
if(message !== null) {
MailPoet.Notice.success(message);
}
}, },
onRestore: function(response) { onRestore: function(response) {
var count = ~~response.newsletters; var count = ~~response;
var message = null; var message = null;
if(count === 1 || response === true) { if(count === 1) {
message = ( message = (
'1 newsletter has been restored from the trash.' '1 newsletter has been restored from the trash.'
); );
} else if(count > 1) { } else {
message = ( message = (
'%$1d newsletters have been restored from the trash.' '%$1d newsletters have been restored from the trash.'
).replace('%$1d', count); ).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
if(message !== null) {
MailPoet.Notice.success(message);
}
} }
}; };

View File

@ -145,14 +145,6 @@ define(
}.bind(this)); }.bind(this));
} }
}, },
componentDidMount: function() {
if(this.isMounted()) {
jQuery('#mailpoet_newsletter').parsley();
}
},
isValid: function() {
return (jQuery('#mailpoet_newsletter').parsley().validate());
},
render: function() { render: function() {
return ( return (
<div> <div>
@ -166,8 +158,7 @@ define(
fields={ fields } fields={ fields }
params={ this.props.params } params={ this.props.params }
messages={ messages } messages={ messages }
isValid={ this.isValid }> >
<p className="submit"> <p className="submit">
<input <input
className="button button-primary" className="button button-primary"

View File

@ -29,7 +29,7 @@ define(
action: 'save', action: 'save',
data: template data: template
}).done(function(response) { }).done(function(response) {
if(response === true) { if(response.result === true) {
this.props.onImport(template); this.props.onImport(template);
} else { } else {
response.map(function(error) { response.map(function(error) {

View File

@ -29,12 +29,14 @@ define(
subject: 'Draft newsletter', subject: 'Draft newsletter',
} }
}).done(function(response) { }).done(function(response) {
if(response.id !== undefined) { if(response.result && response.newsletter.id) {
this.history.pushState(null, `/template/${response.id}`); this.history.pushState(null, `/template/${response.newsletter.id}`);
} else { } else {
response.map(function(error) { if(response.errors.length > 0) {
MailPoet.Notice.error(error); response.errors.map(function(error) {
}); MailPoet.Notice.error(error);
});
}
} }
}.bind(this)); }.bind(this));
}, },

View File

@ -138,12 +138,14 @@ define(
options: this.state, options: this.state,
}, },
}).done(function(response) { }).done(function(response) {
if(response.id !== undefined) { if(response.result && response.newsletter.id) {
this.showTemplateSelection(response.id); this.showTemplateSelection(response.newsletter.id);
} else { } else {
response.map(function(error) { if(response.errors.length > 0) {
MailPoet.Notice.error(error); response.errors.map(function(error) {
}); MailPoet.Notice.error(error);
});
}
} }
}.bind(this)); }.bind(this));
}, },

View File

@ -32,12 +32,15 @@ define(
type: 'standard', type: 'standard',
} }
}).done(function(response) { }).done(function(response) {
if(response.id !== undefined) { console.log(response);
this.showTemplateSelection(response.id); if(response.result && response.newsletter.id) {
this.showTemplateSelection(response.newsletter.id);
} else { } else {
response.map(function(error) { if(response.errors.length > 0) {
MailPoet.Notice.error(error); response.errors.map(function(error) {
}); MailPoet.Notice.error(error);
});
}
} }
}.bind(this)); }.bind(this));
}, },

View File

@ -111,12 +111,14 @@ define(
options: this.state, options: this.state,
}, },
}).done(function(response) { }).done(function(response) {
if(response.id !== undefined) { if(response.result && response.newsletter.id) {
this.showTemplateSelection(response.id); this.showTemplateSelection(response.newsletter.id);
} else { } else {
response.map(function(error) { if(response.errors.length > 0) {
MailPoet.Notice.error(error); response.errors.map(function(error) {
}); MailPoet.Notice.error(error);
});
}
} }
}.bind(this)); }.bind(this));
}, },

View File

@ -192,21 +192,28 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
error: function(message, options) { error: function(message, options) {
this.show(jQuery.extend({}, { this.show(jQuery.extend({}, {
type: 'error', type: 'error',
message: '<p>'+message+'</p>' message: '<p>'+this.formatMessage(message)+'</p>'
}, options)); }, options));
}, },
success: function(message, options) { success: function(message, options) {
this.show(jQuery.extend({}, { this.show(jQuery.extend({}, {
type: 'success', type: 'success',
message: '<p>'+message+'</p>' message: '<p>'+this.formatMessage(message)+'</p>'
}, options)); }, options));
}, },
system: function(message, options) { system: function(message, options) {
this.show(jQuery.extend({}, { this.show(jQuery.extend({}, {
type: 'system', type: 'system',
static: true, static: true,
message: message message: '<p>'+this.formatMessage(message)+'</p>'
}, options)); }, options));
},
formatMessage: function(message) {
if(Array.isArray(message)) {
return message.join('<br />');
} else {
return message;
}
} }
}; };
}); });

View File

@ -20,10 +20,7 @@ function(
$('form.mailpoet_form').each(function() { $('form.mailpoet_form').each(function() {
var form = $(this); var form = $(this);
form.parsley({ form.parsley().on('form:submit', function(parsley) {
errorsWrapper: '<p></p>',
errorTemplate: '<span></span>'
}).on('form:submit', function(parsley) {
var data = form.serializeObject() || {}; var data = form.serializeObject() || {};

View File

@ -12,7 +12,7 @@ define(
Form Form
) { ) {
var fields = [ let fields = [
{ {
name: 'name', name: 'name',
label: 'Name', label: 'Name',
@ -25,7 +25,7 @@ define(
} }
]; ];
var messages = { const messages = {
onUpdate: function() { onUpdate: function() {
MailPoet.Notice.success('Segment successfully updated!'); MailPoet.Notice.success('Segment successfully updated!');
}, },
@ -34,7 +34,7 @@ define(
} }
}; };
var SegmentForm = React.createClass({ const SegmentForm = React.createClass({
mixins: [ mixins: [
Router.History Router.History
], ],

View File

@ -42,58 +42,49 @@ var columns = [
const messages = { const messages = {
onTrash: function(response) { onTrash: function(response) {
if(response) { var count = ~~response;
let message = null; var message = null;
if(~~response === 1) {
message = (
'1 segment was moved to the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d segments were moved to the trash.'
).replace('%$1d', ~~response);
}
if(message !== null) { if(count === 1) {
MailPoet.Notice.success(message); message = (
} '1 segment was moved to the trash.'
);
} else {
message = (
'%$1d segments were moved to the trash.'
).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
}, },
onDelete: function(response) { onDelete: function(response) {
if(response) { var count = ~~response;
let message = null; var message = null;
if(~~response === 1) {
message = (
'1 segment was permanently deleted.'
);
} else if(~~response > 1) {
message = (
'%$1d segments were permanently deleted.'
).replace('%$1d', ~~response);
}
if(message !== null) { if(count === 1) {
MailPoet.Notice.success(message); message = (
} '1 segment was permanently deleted.'
);
} else {
message = (
'%$1d segments were permanently deleted.'
).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
}, },
onRestore: function(response) { onRestore: function(response) {
if(response) { var count = ~~response;
let message = null; var message = null;
if(~~response === 1) {
message = (
'1 segment has been restored from the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d segments have been restored from the trash.'
).replace('%$1d', ~~response);
}
if(message !== null) { if(count === 1) {
MailPoet.Notice.success(message); message = (
} '1 segment has been restored from the trash.'
);
} else {
message = (
'%$1d segments have been restored from the trash.'
).replace('%$1d', count);
} }
MailPoet.Notice.success(message);
} }
}; };
@ -105,6 +96,9 @@ const item_actions = [
return ( return (
<Link to={ `/edit/${item.id}` }>Edit</Link> <Link to={ `/edit/${item.id}` }>Edit</Link>
); );
},
display: function(segment) {
return (segment.type !== 'wp_users');
} }
}, },
{ {

View File

@ -51,6 +51,24 @@ define(
} }
]; ];
var custom_fields = window.mailpoet_custom_fields || [];
custom_fields.map(custom_field => {
let field = {
name: 'cf_' + custom_field.id,
label: custom_field.name,
type: custom_field.type
};
if(custom_field.params) {
field.params = custom_field.params;
}
if(custom_field.params.values) {
field.values = custom_field.params.values;
}
fields.push(field);
});
var messages = { var messages = {
onUpdate: function() { onUpdate: function() {
MailPoet.Notice.success('Subscriber successfully updated!'); MailPoet.Notice.success('Subscriber successfully updated!');

View File

@ -8,12 +8,14 @@ define(
'papaparse', 'papaparse',
'select2' 'select2'
], ],
function (Backbone, function (
_, Backbone,
jQuery, _,
MailPoet, jQuery,
Handlebars, MailPoet,
Papa) { Handlebars,
Papa
) {
if (!jQuery('#mailpoet_subscribers_import').length) { if (!jQuery('#mailpoet_subscribers_import').length) {
return; return;
} }
@ -138,6 +140,14 @@ define(
*/ */
uploadElement.change(function () { uploadElement.change(function () {
MailPoet.Notice.hide(); MailPoet.Notice.hide();
var ext = this.value.match(/\.(.+)$/);
if (ext === null || ext[1].toLowerCase() !== 'csv') {
this.value = '';
MailPoet.Notice.error(MailPoetI18n.wrongFileFormat, {
timeout: 3000,
});
}
toggleNextStepButton( toggleNextStepButton(
uploadProcessButtonElement, uploadProcessButtonElement,
(this.value.trim() !== '') ? 'on' : 'off' (this.value.trim() !== '') ? 'on' : 'off'
@ -183,7 +193,7 @@ define(
}).done(function (response) { }).done(function (response) {
if (response.result === false) { if (response.result === false) {
MailPoet.Notice.hide(); MailPoet.Notice.hide();
MailPoet.Notice.error(response.error, { MailPoet.Notice.error(response.errors, {
timeout: 3000, timeout: 3000,
}); });
jQuery('.mailpoet_mailchimp-key-status') jQuery('.mailpoet_mailchimp-key-status')
@ -235,7 +245,7 @@ define(
} }
else { else {
MailPoet.Notice.hide(); MailPoet.Notice.hide();
MailPoet.Notice(response.message, { MailPoet.Notice.error(response.errors, {
timeout: 3000, timeout: 3000,
}); });
} }
@ -416,7 +426,10 @@ define(
} }
else { else {
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
MailPoet.Notice.error(MailPoetI18n.noValidRecords, { var errorNotice = MailPoetI18n.noValidRecords;
errorNotice = errorNotice.replace('[link]', MailPoetI18n.csvKBLink);
errorNotice = errorNotice.replace('[/link]', '</a>');
MailPoet.Notice.error(errorNotice, {
timeout: 3000, timeout: 3000,
}); });
} }
@ -721,7 +734,7 @@ define(
} }
}); });
// reduce subscribers object if the total length is geater than the // reduce subscribers object if the total length is greater than the
// maximum number of defined rows // maximum number of defined rows
if (subscribers.subscribersCount > (maxRowsToShow + 1)) { if (subscribers.subscribersCount > (maxRowsToShow + 1)) {
subscribers.subscribers.splice( subscribers.subscribers.splice(
@ -1069,7 +1082,7 @@ define(
}).done(function (response) { }).done(function (response) {
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
if (response.result === false) { if (response.result === false) {
MailPoet.Notice.error(response.error, { MailPoet.Notice.error(response.errors, {
timeout: 3000, timeout: 3000,
}); });
} else { } else {

View File

@ -19,3 +19,12 @@ modules:
user: '' user: ''
password: '' password: ''
dump: tests/_data/dump.sql dump: tests/_data/dump.sql
coverage:
enabled: true
whitelist:
include:
- lib/*
exclude:
blacklist:
include:
exclude:

View File

@ -10,7 +10,8 @@
"swiftmailer/swiftmailer": "^5.4", "swiftmailer/swiftmailer": "^5.4",
"phpseclib/phpseclib": "*", "phpseclib/phpseclib": "*",
"mtdowling/cron-expression": "^1.0", "mtdowling/cron-expression": "^1.0",
"nesbot/carbon": "^1.21" "nesbot/carbon": "^1.21",
"soundasleep/html2text": "^0.3.0"
}, },
"require-dev": { "require-dev": {
"codeception/codeception": "*", "codeception/codeception": "*",

672
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,13 +9,6 @@ class Activator {
function __construct() { function __construct() {
} }
function init() {
register_activation_hook(
Env::$file,
array($this, 'activate')
);
}
function activate() { function activate() {
$migrator = new Migrator(); $migrator = new Migrator();
$migrator->up(); $migrator->up();

View File

@ -36,8 +36,9 @@ class Env {
self::$views_path = self::$path . '/views'; self::$views_path = self::$path . '/views';
self::$assets_path = self::$path . '/assets'; self::$assets_path = self::$path . '/assets';
self::$assets_url = plugins_url('/assets', $file); self::$assets_url = plugins_url('/assets', $file);
self::$temp_path = wp_upload_dir()['path']; $wp_upload_dir = wp_upload_dir();
self::$temp_URL = wp_upload_dir()['url']; self::$temp_path = $wp_upload_dir['path'];
self::$temp_URL = $wp_upload_dir['url'];
self::$languages_path = self::$path . '/lang'; self::$languages_path = self::$path . '/lang';
self::$lib_path = self::$path . '/lib'; self::$lib_path = self::$path . '/lib';
self::$plugin_prefix = 'mailpoet_'; self::$plugin_prefix = 'mailpoet_';
@ -72,19 +73,4 @@ class Env {
); );
return implode('', $source_name); return implode('', $source_name);
} }
static function isPluginActivated() {
$activatesPlugins = get_option('active_plugins');
$isActivated = (
in_array(
sprintf('%s/%s.php', basename(self::$path), self::$plugin_name),
$activatesPlugins
) ||
in_array(
sprintf('%s/%s.php', explode('/', plugin_basename(__FILE__))[0], self::$plugin_name),
$activatesPlugins
)
);
return ($isActivated) ? true : false;
}
} }

View File

@ -9,10 +9,17 @@ class Hooks {
function init() { function init() {
// Subscribe in comments // Subscribe in comments
if((bool)Setting::getValue('subscribe.on_comment.enabled')) { if((bool)Setting::getValue('subscribe.on_comment.enabled')) {
add_action( if(is_user_logged_in()) {
'comment_form_after_fields', add_action(
'\MailPoet\Subscription\Comment::extendForm' 'comment_form_field_comment',
); '\MailPoet\Subscription\Comment::extendLoggedInForm'
);
} else {
add_action(
'comment_form_after_fields',
'\MailPoet\Subscription\Comment::extendLoggedOutForm'
);
}
add_action( add_action(
'comment_post', 'comment_post',

View File

@ -9,6 +9,8 @@ use MailPoet\Settings\Pages;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-admin/includes/plugin.php');
class Initializer { class Initializer {
function __construct($params = array( function __construct($params = array(
'file' => '', 'file' => '',
@ -19,21 +21,34 @@ class Initializer {
function init() { function init() {
$this->setupDB(); $this->setupDB();
$this->setupActivator();
$this->setupRenderer(); register_activation_hook(Env::$file, array($this, 'runMigrator'));
$this->setupLocalizer(); register_activation_hook(Env::$file, array($this, 'runPopulator'));
$this->setupMenu();
$this->setupRouter(); add_action('plugins_loaded', array($this, 'setup'));
$this->setupWidget(); add_action('widgets_init', array($this, 'setupWidget'));
$this->setupAnalytics(); }
$this->setupPermissions();
$this->setupChangelog(); function setup() {
$this->setupPublicAPI(); try {
$this->runQueueSupervisor(); $this->setupRenderer();
$this->setupShortcodes(); $this->setupLocalizer();
$this->setupHooks(); $this->setupMenu();
$this->setupPages(); $this->setupRouter();
$this->setupImages(); $this->setupPermissions();
$this->setupPublicAPI();
$this->setupAnalytics();
$this->setupChangelog();
$this->runQueueSupervisor();
$this->setupShortcodes();
$this->setupHooks();
$this->setupPages();
$this->setupImages();
} catch(\Exception $e) {
// if anything goes wrong during init
// automatically deactivate the plugin
deactivate_plugins(Env::$file);
}
} }
function setupDB() { function setupDB() {
@ -51,8 +66,6 @@ class Initializer {
$newsletters = Env::$db_prefix . 'newsletters'; $newsletters = Env::$db_prefix . 'newsletters';
$newsletter_templates = Env::$db_prefix . 'newsletter_templates'; $newsletter_templates = Env::$db_prefix . 'newsletter_templates';
$segments = Env::$db_prefix . 'segments'; $segments = Env::$db_prefix . 'segments';
$filters = Env::$db_prefix . 'filters';
$segment_filter = Env::$db_prefix . 'segment_filter';
$forms = Env::$db_prefix . 'forms'; $forms = Env::$db_prefix . 'forms';
$subscriber_segment = Env::$db_prefix . 'subscriber_segment'; $subscriber_segment = Env::$db_prefix . 'subscriber_segment';
$newsletter_segment = Env::$db_prefix . 'newsletter_segment'; $newsletter_segment = Env::$db_prefix . 'newsletter_segment';
@ -67,8 +80,6 @@ class Initializer {
define('MP_SETTINGS_TABLE', $settings); define('MP_SETTINGS_TABLE', $settings);
define('MP_NEWSLETTERS_TABLE', $newsletters); define('MP_NEWSLETTERS_TABLE', $newsletters);
define('MP_SEGMENTS_TABLE', $segments); define('MP_SEGMENTS_TABLE', $segments);
define('MP_FILTERS_TABLE', $filters);
define('MP_SEGMENT_FILTER_TABLE', $segment_filter);
define('MP_FORMS_TABLE', $forms); define('MP_FORMS_TABLE', $forms);
define('MP_SUBSCRIBER_SEGMENT_TABLE', $subscriber_segment); define('MP_SUBSCRIBER_SEGMENT_TABLE', $subscriber_segment);
define('MP_NEWSLETTER_TEMPLATES_TABLE', $newsletter_templates); define('MP_NEWSLETTER_TEMPLATES_TABLE', $newsletter_templates);
@ -81,9 +92,14 @@ class Initializer {
define('MP_NEWSLETTER_STATISTICS_TABLE', $newsletter_statistics); define('MP_NEWSLETTER_STATISTICS_TABLE', $newsletter_statistics);
} }
function setupActivator() { function runMigrator() {
$activator = new Activator(); $migrator = new Migrator();
$activator->init(); $migrator->up();
}
function runPopulator() {
$populator = new Populator();
$populator->up();
} }
function setupRenderer() { function setupRenderer() {
@ -97,10 +113,7 @@ class Initializer {
} }
function setupMenu() { function setupMenu() {
$menu = new Menu( $menu = new Menu($this->renderer, Env::$assets_url);
$this->renderer,
Env::$assets_url
);
$menu->init(); $menu->init();
} }
@ -138,6 +151,7 @@ class Initializer {
$shortcodes = new Shortcodes(); $shortcodes = new Shortcodes();
$shortcodes->init(); $shortcodes->init();
} }
function setupHooks() { function setupHooks() {
$hooks = new Hooks(); $hooks = new Hooks();
$hooks->init(); $hooks->init();
@ -153,7 +167,7 @@ class Initializer {
try { try {
$supervisor = new Supervisor(); $supervisor = new Supervisor();
$supervisor->checkDaemon(); $supervisor->checkDaemon();
} catch (\Exception $e) { } catch(\Exception $e) {
} }
} }

View File

@ -11,8 +11,7 @@ class Localizer {
function init() { function init() {
add_action( add_action(
'init', 'init',
array($this, 'setup'), array($this, 'setup')
0
); );
} }

View File

@ -296,7 +296,7 @@ class Menu {
} else { } else {
// check if users can register // check if users can register
$flags['registration_enabled'] = $flags['registration_enabled'] =
(bool) get_option('users_can_register', false); (bool)get_option('users_can_register', false);
} }
return $flags; return $flags;
@ -307,6 +307,23 @@ class Menu {
$data['segments'] = Segment::findArray(); $data['segments'] = Segment::findArray();
$data['custom_fields'] = array_map(function($field) {
$field['params'] = unserialize($field['params']);
if(!empty($field['params']['values'])) {
$values = array();
foreach($field['params']['values'] as $value) {
$values[$value['value']] = $value['value'];
}
$field['params']['values'] = $values;
}
return $field;
}, CustomField::findArray());
$data['date_formats'] = Block\Date::getDateFormats();
$data['month_names'] = Block\Date::getMonthNames();
echo $this->renderer->render('subscribers/subscribers.html', $data); echo $this->renderer->render('subscribers/subscribers.html', $data);
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace MailPoet\Config; namespace MailPoet\Config;
if (!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
@ -236,10 +236,7 @@ class Migrator {
'newsletter_id mediumint(9) NOT NULL,', 'newsletter_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,', 'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,', 'queue_id mediumint(9) NOT NULL,',
'sent_at TIMESTAMP NOT NULL DEFAULT 0,', 'sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'PRIMARY KEY (id)', 'PRIMARY KEY (id)',
); );
return $this->sqlify(__FUNCTION__, $attributes); return $this->sqlify(__FUNCTION__, $attributes);

View File

@ -68,7 +68,7 @@ class FranksRoastHouseTemplate {
"link" => "http://www.example.com", "link" => "http://www.example.com",
"src" => $this->template_image_url . "/header-v2.jpg", "src" => $this->template_image_url . "/header-v2.jpg",
"alt" => __("Frank's Roast House"), "alt" => __("Frank's Roast House"),
"padded" => false, "fullWidth" => true,
"width" => "600px", "width" => "600px",
"height" => "220px", "height" => "220px",
"styles" => array( "styles" => array(
@ -95,7 +95,7 @@ class FranksRoastHouseTemplate {
"link" => "http://example.org", "link" => "http://example.org",
"src" => $this->template_image_url . "/coffee-grain.jpg", "src" => $this->template_image_url . "/coffee-grain.jpg",
"alt" => __("coffee-grain-3-1329675-1599x941"), "alt" => __("coffee-grain-3-1329675-1599x941"),
"padded" => true, "fullWidth" => false,
"width" => "1599px", "width" => "1599px",
"height" => "777px", "height" => "777px",
"styles" => array( "styles" => array(
@ -139,7 +139,7 @@ class FranksRoastHouseTemplate {
"link" => "http://example.org", "link" => "http://example.org",
"src" => $this->template_image_url . "/sandwich.jpg", "src" => $this->template_image_url . "/sandwich.jpg",
"alt" => "sandwich", "alt" => "sandwich",
"padded" => true, "fullWidth" => false,
"width" => "640px", "width" => "640px",
"height" => "344px", "height" => "344px",
"styles" => array( "styles" => array(
@ -238,7 +238,7 @@ class FranksRoastHouseTemplate {
"link" => "http://example.org", "link" => "http://example.org",
"src" => $this->template_image_url . "/map-v2.jpg", "src" => $this->template_image_url . "/map-v2.jpg",
"alt" => __("map-v2"), "alt" => __("map-v2"),
"padded" => true, "fullWidth" => false,
"width" => "636px", "width" => "636px",
"height" => "342px", "height" => "342px",
"styles" => array( "styles" => array(
@ -279,19 +279,19 @@ class FranksRoastHouseTemplate {
"blocks" => array( "blocks" => array(
array( array(
"type" => "footer", "type" => "footer",
"text" => __("<p><span style=\"color: #000000;\"><a href=\"[unsubscribeUrl]\" style=\"color: #000000;\">Unsubscribe</a> | <a href=\"[manageSubscriptionUrl]\" style=\"color: #000000;\">Manage subscription</a></span><br /><span style=\"color: #000000;\">12345 MailPoet Drive, EmailVille, 76543</span></p>"), "text" => __("<p><a href=\"[unsubscribeUrl]\">Unsubscribe</a> | <a href=\"[manageSubscriptionUrl]\">Manage subscription</a><br />12345 MailPoet Drive, EmailVille, 76543</p>"),
"styles" => array( "styles" => array(
"block" => array( "block" => array(
"backgroundColor" => "#a9a7a7" "backgroundColor" => "#a9a7a7"
), ),
"text" => array( "text" => array(
"fontColor" => "#ffffff", "fontColor" => "#000000",
"fontFamily" => "Arial", "fontFamily" => "Arial",
"fontSize" => "12px", "fontSize" => "12px",
"textAlign" => "center" "textAlign" => "center"
), ),
"link" => array( "link" => array(
"fontColor" => "#ffffff", "fontColor" => "#000000",
"textDecoration" => "underline" "textDecoration" => "underline"
) )
) )

View File

@ -90,7 +90,7 @@ class PostNotificationsBlankTemplate {
"link" => "http://example.org", "link" => "http://example.org",
"src" => $this->template_image_url . "/ALC-widget-icon.png", "src" => $this->template_image_url . "/ALC-widget-icon.png",
"alt" => __("ALC-widget-icon"), "alt" => __("ALC-widget-icon"),
"padded" => true, "fullWidth" => false,
"width" => "200px", "width" => "200px",
"height" => "134px", "height" => "134px",
"styles" => array( "styles" => array(

View File

@ -68,7 +68,7 @@ class WelcomeTemplate {
"link" => "http://example.org", "link" => "http://example.org",
"src" => $this->template_image_url . "/logo-header.gif", "src" => $this->template_image_url . "/logo-header.gif",
"alt" => "logo-header", "alt" => "logo-header",
"padded" => true, "fullWidth" => false,
"width" => "233px", "width" => "233px",
"height" => "118px", "height" => "118px",
"styles" => array( "styles" => array(

View File

@ -20,7 +20,7 @@ class PublicAPI {
Helpers::underscoreToCamelCase($_GET['action']) : Helpers::underscoreToCamelCase($_GET['action']) :
false; false;
$this->request_payload = isset($_GET['request_payload']) ? $this->request_payload = isset($_GET['request_payload']) ?
json_decode(urldecode($_GET['request_payload']), true) : unserialize(base64_decode($_GET['request_payload'])) :
false; false;
} }

View File

@ -9,13 +9,12 @@ class Widget {
} }
function init() { function init() {
add_action('widgets_init', array($this, 'registerWidget')); $this->registerWidget();
if(!is_admin()) { if(!is_admin()) {
//$this->setupActions(); $this->setupDependencies();
add_action('widgets_init', array($this, 'setupDependencies'));
} else { } else {
add_action('widgets_init', array($this, 'setupAdminDependencies')); $this->setupAdminDependencies();
} }
} }
@ -69,6 +68,9 @@ class Widget {
} }
} }
// TODO: extract this method into an Initializer
// - the "ajax" part might probably be useless
// - the "post" (non-ajax) part needs to be redone properly
function setupActions() { function setupActions() {
// ajax requests // ajax requests
add_action( add_action(

71
lib/Cron/CronHelper.php Normal file
View File

@ -0,0 +1,71 @@
<?php
namespace MailPoet\Cron;
use MailPoet\Models\Setting;
use MailPoet\Util\Security;
if(!defined('ABSPATH')) exit;
class CronHelper {
const daemon_execution_limit = 20;
const daemon_execution_timeout = 35;
const daemon_request_timeout = 2;
static function createDaemon($token) {
$daemon = array(
'status' => 'starting',
'counter' => 0,
'token' => $token
);
self::saveDaemon($daemon);
return $daemon;
}
static function getDaemon() {
return Setting::getValue('cron_daemon');
}
static function saveDaemon($daemon) {
$daemon['updated_at'] = time();
return Setting::setValue(
'cron_daemon',
$daemon
);
}
static function createToken() {
return Security::generateRandomString();
}
static function accessDaemon($token, $timeout = self::daemon_request_timeout) {
$payload = serialize(array('token' => $token));
$url = '/?mailpoet-api&section=queue&action=run&request_payload=' .
base64_encode($payload);
$args = array(
'timeout' => $timeout,
'user-agent' => 'MailPoet (www.mailpoet.com) Cron'
);
$result = wp_remote_get(
self::getSiteUrl() . $url,
$args
);
return wp_remote_retrieve_body($result);
}
private static function getSiteUrl() {
// additional check for some sites running on a virtual machine or behind
// proxy where there could be different ports (e.g., host:8080 => guest:80)
// if the site URL does not contain a port, return the URL
if(!preg_match('!^https?://.*?:\d+!', site_url())) return site_url();
preg_match('!://(?P<host>.*?):(?P<port>\d+)!', site_url(), $server);
// connect to the URL with port
$fp = @fsockopen($server['host'], $server['port'], $errno, $errstr, 1);
if($fp) return site_url();
// connect to the URL without port
$fp = @fsockopen($server['host'], $server['port'], $errno, $errstr, 1);
if($fp) return preg_replace('!(?=:\d+):\d+!', '$1', site_url());
// throw an error if all connection attempts failed
throw new \Exception(__('Site URL is unreachable.'));
}
}

View File

@ -2,8 +2,6 @@
namespace MailPoet\Cron; namespace MailPoet\Cron;
use MailPoet\Cron\Workers\SendingQueue; use MailPoet\Cron\Workers\SendingQueue;
use MailPoet\Models\Setting;
use MailPoet\Util\Security;
require_once(ABSPATH . 'wp-includes/pluggable.php'); require_once(ABSPATH . 'wp-includes/pluggable.php');
@ -13,132 +11,69 @@ class Daemon {
public $daemon; public $daemon;
public $request_payload; public $request_payload;
public $refreshed_token; public $refreshed_token;
public $timer; const daemon_request_timeout = 5;
private $timer;
function __construct($request_payload = array()) { function __construct($request_payload = array()) {
set_time_limit(0); set_time_limit(0);
ignore_user_abort(); ignore_user_abort();
$this->daemon = $this->getDaemon(); $this->daemon = CronHelper::getDaemon();
$this->refreshed_token = $this->refreshToken(); $this->token = CronHelper::createToken();
$this->request_payload = $request_payload; $this->request_payload = $request_payload;
$this->timer = microtime(true); $this->timer = microtime(true);
} }
function start() { function run() {
if(!isset($this->request_payload['session'])) {
$this->abortWithError(__('Missing session ID.'));
}
$this->manageSession('start');
$daemon = $this->daemon; $daemon = $this->daemon;
if(!$daemon) { if(!$daemon) {
$this->saveDaemon( $this->abortWithError(__('Daemon does not exist.'));
array(
'status' => 'starting',
'counter' => 0
)
);
}
if($daemon['status'] === 'started') {
$_SESSION['cron_daemon'] = array(
'result' => false,
'errors' => array(__('Daemon already running.'))
);
}
if($daemon['status'] === 'starting') {
$_SESSION['cron_daemon'] = 'started';
$_SESSION['cron_daemon'] = array('result' => true);
$this->manageSession('end');
$daemon['status'] = 'started';
$daemon['token'] = $this->refreshed_token;
$this->saveDaemon($daemon);
$this->callSelf();
}
$this->manageSession('end');
}
function run() {
$allowed_statuses = array(
'stopping',
'starting',
'started'
);
if(!$this->daemon || !in_array($this->daemon['status'], $allowed_statuses)) {
$this->abortWithError(__('Invalid daemon status.'));
} }
if(!isset($this->request_payload['token']) || if(!isset($this->request_payload['token']) ||
$this->request_payload['token'] !== $this->daemon['token'] $this->request_payload['token'] !== $daemon['token']
) { ) {
$this->abortWithError('Invalid token.'); $this->abortWithError(__('Invalid or missing token.'));
} }
$this->abortIfStopped($daemon);
try { try {
$sending_queue = new SendingQueue($this->timer); $sending_queue = new SendingQueue($this->timer);
$sending_queue->process(); $sending_queue->process();
} catch(Exception $e) { } catch(\Exception $e) {
} }
$elapsed_time = microtime(true) - $this->timer; $elapsed_time = microtime(true) - $this->timer;
if($elapsed_time < 30) { if($elapsed_time < CronHelper::daemon_execution_limit) {
sleep(30 - $elapsed_time); sleep(CronHelper::daemon_execution_limit - $elapsed_time);
}
// after each execution, re-read daemon data in case it was deleted or
// its status has changed
$daemon = CronHelper::getDaemon();
if(!$daemon || $daemon['token'] !== $this->request_payload['token']) {
exit;
} }
// after each execution, read daemon in case its status was modified
$daemon = $this->getDaemon();
if($daemon['status'] === 'stopping') $daemon['status'] = 'stopped';
if($daemon['status'] === 'starting') $daemon['status'] = 'started';
$daemon['token'] = $this->refreshed_token;
$daemon['counter']++; $daemon['counter']++;
$this->abortIfStopped($daemon);
$this->saveDaemon($daemon); if($daemon['status'] === 'starting') {
$daemon['status'] = 'started';
if($daemon['status'] === 'started') $this->callSelf();
}
function getDaemon() {
return Setting::getValue('cron_daemon');
}
function saveDaemon($daemon_data) {
$daemon_data['updated_at'] = time();
return Setting::setValue(
'cron_daemon',
$daemon_data
);
}
function refreshToken() {
return Security::generateRandomString();
}
function manageSession($action) {
switch($action) {
case 'start':
if(session_id()) {
session_write_close();
}
session_id($this->request_payload['session']);
session_start();
break;
case 'end':
session_write_close();
break;
} }
$daemon['token'] = $this->token;
CronHelper::saveDaemon($daemon);
$this->callSelf();
}
function abortIfStopped($daemon) {
if($daemon['status'] === 'stopped') exit;
if($daemon['status'] === 'stopping') {
$daemon['status'] = 'stopped';
CronHelper::saveDaemon($daemon);
exit;
}
}
function abortWithError($message) {
exit('[mailpoet_cron_error:' . base64_encode($message) . ']');
} }
function callSelf() { function callSelf() {
$payload = json_encode(array('token' => $this->refreshed_token)); CronHelper::accessDaemon($this->token, self::daemon_request_timeout);
Supervisor::accessRemoteUrl(
'/?mailpoet-api&section=queue&action=run&request_payload=' . urlencode($payload)
);
exit;
}
function abortWithError($error) {
wp_send_json(
array(
'result' => false,
'errors' => array($error)
));
exit; exit;
} }
} }

View File

@ -1,103 +1,94 @@
<?php <?php
namespace MailPoet\Cron; namespace MailPoet\Cron;
use MailPoet\Config\Env;
use MailPoet\Models\Setting;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class Supervisor { class Supervisor {
public $daemon; public $daemon;
public $token;
public $force_run;
function __construct($force_start = false) { function __construct($force_run = false) {
$this->force_start = $force_start; $this->daemon = CronHelper::getDaemon();
if(!Env::isPluginActivated()) { $this->token = CronHelper::createToken();
throw new \Exception(__('MailPoet is not activated.')); $this->force_run = $force_run;
}
$this->daemon = $this->getDaemon();
} }
function checkDaemon() { function checkDaemon() {
if(!$this->daemon) { $daemon = $this->daemon;
return $this->startDaemon(); if(!$daemon) {
$daemon = CronHelper::createDaemon($this->token);
return $this->runDaemon($daemon);
} }
if( // if the daemon is stopped, return its status and do nothing
!$this->force_start && if(!$this->force_run &&
in_array($this->daemon['status'], array('stopped', 'stopping')) isset($daemon['status']) &&
$daemon['status'] === 'stopped'
) { ) {
return $this->daemon['status']; return $this->formatDaemonStatusMessage($daemon['status']);
} }
$elapsed_time = time() - (int) $daemon['updated_at'];
$elapsed_time = time() - (int)$this->daemon['updated_at']; // if it's been less than 40 seconds since last execution and we're not
// force-running the daemon, return its status and do nothing
if($elapsed_time < 40) { if($elapsed_time < CronHelper::daemon_execution_timeout && !$this->force_run) {
if(!$this->force_start) { return $this->formatDaemonStatusMessage($daemon['status']);
return;
}
if($this->daemon['status'] === 'stopping' ||
$this->daemon['status'] === 'starting'
) {
return $this->daemon['status'];
}
} }
$this->daemon['status'] = 'starting'; // if it's been less than 40 seconds since last execution, we are
$this->saveDaemon($this->daemon); // force-running the daemon and it's either being started or stopped,
return $this->startDaemon(); // return its status and do nothing
elseif($elapsed_time < CronHelper::daemon_execution_timeout &&
$this->force_run &&
in_array($daemon['status'], array(
'stopping',
'starting'
))
) {
return $this->formatDaemonStatusMessage($daemon['status']);
}
// re-create (restart) daemon
CronHelper::createDaemon($this->token);
return $this->runDaemon();
} }
function startDaemon() { function runDaemon() {
if(!session_id()) session_start(); $request = CronHelper::accessDaemon($this->token);
$sessionId = session_id(); preg_match('/\[(mailpoet_cron_error:.*?)\]/i', $request, $status);
session_write_close(); $daemon = CronHelper::getDaemon();
$_SESSION['cron_daemon'] = null; if(!empty($status) || !$daemon) {
$requestPayload = json_encode(array('session' => $sessionId)); if(!$daemon) {
self::accessRemoteUrl( $message = __('Daemon failed to run.');
'/?mailpoet-api&section=queue&action=start&request_payload=' . } else {
urlencode($requestPayload) list(, $message) = explode(':', $status[0]);
); $message = base64_decode($message);
session_start(); }
$daemonStatus = $_SESSION['cron_daemon']; return $this->formatResultMessage(
unset($_SESSION['daemon']); false,
session_write_close(); $message
return $daemonStatus; );
}
return $this->formatDaemonStatusMessage($daemon['status']);
} }
function getDaemon() { private function formatDaemonStatusMessage($status) {
return Setting::getValue('cron_daemon'); return $this->formatResultMessage(
} true,
sprintf(
function saveDaemon($daemon_data) { __('Daemon is currently %s.'),
return Setting::setValue( __($status)
'cron_daemon', )
$daemon_data
); );
} }
static function accessRemoteUrl($url) { private function formatResultMessage($result, $message) {
$args = array( $formattedResult = array(
'timeout' => 1, 'result' => $result
'user-agent' => 'MailPoet (www.mailpoet.com) Cron'
); );
wp_remote_get( if(!$result) {
self::getSiteUrl() . $url, $formattedResult['errors'] = array($message);
$args } else {
); $formattedResult['message'] = $message;
} }
return $formattedResult;
static function getSiteUrl() {
// additional check for some sites running on a virtual machine or behind
// proxy where there could be different ports (e.g., host:8080 => guest:80)
// if the site URL does not contain a port, return the URL
if(!preg_match('!^https?://.*?:\d+!', site_url())) return site_url();
preg_match('!://(?P<host>.*?):(?P<port>\d+)!', site_url(), $server);
// connect to the URL with port
$fp = @fsockopen($server['host'], $server['port'], $errno, $errstr, 1);
if($fp) return site_url();
// connect to the URL without port
$fp = @fsockopen($server['host'], $server['port'], $errno, $errstr, 1);
if($fp) return preg_replace('!(?=:\d+):\d+!', '$1', site_url());
// throw an error if all connections fail
throw new \Exception(__('Site URL is unreachable.'));
} }
} }

View File

@ -1,123 +1,190 @@
<?php <?php
namespace MailPoet\Cron\Workers; namespace MailPoet\Cron\Workers;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer; use MailPoet\Mailer\Mailer;
use MailPoet\Models\Newsletter; use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterStatistics; use MailPoet\Models\NewsletterStatistics;
use MailPoet\Models\Setting;
use MailPoet\Models\Subscriber; use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Renderer\Renderer; use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class SendingQueue { class SendingQueue {
public $timer; public $mta_config;
public $mta_log;
public $processing_method;
private $timer;
const batch_size = 50;
function __construct($timer = false) { function __construct($timer = false) {
$this->mta_config = $this->getMailerConfig();
$this->mta_log = $this->getMailerLog();
$this->processing_method = ($this->mta_config['method'] === 'MailPoet') ?
'processBulkSubscribers' :
'processIndividualSubscriber';
$this->timer = ($timer) ? $timer : microtime(true); $this->timer = ($timer) ? $timer : microtime(true);
} }
function process() { function process() {
// TODO: implement mailer sending frequency limits
foreach($this->getQueues() as $queue) { foreach($this->getQueues() as $queue) {
$newsletter = Newsletter::findOne($queue->newsletter_id) $newsletter = Newsletter::findOne($queue->newsletter_id);
->asArray();
if(!$newsletter) { if(!$newsletter) {
continue; continue;
}; }
$mailer = $this->configureMailerForNewsletter($newsletter); $queue->subscribers = (object) unserialize($queue->subscribers);
$newsletter = $this->renderNewsletter($newsletter); if(!isset($queue->subscribers->processed)) {
$subscribers = json_decode($queue->subscribers, true); $queue->subscribers->processed = array();
$subscribers_to_process = $subscribers['to_process']; }
if(!isset($subscribers['processed'])) $subscribers['processed'] = array(); if(!isset($queue->subscribers->failed)) {
if(!isset($subscribers['failed'])) $subscribers['failed'] = array(); $queue->subscribers->failed = array();
foreach(array_chunk($subscribers_to_process, 200) as $subscriber_ids) { }
$db_subscribers = Subscriber::whereIn('id', $subscriber_ids) $newsletter = $newsletter->asArray();
$newsletter['body'] = $this->renderNewsletter($newsletter);
$mailer = $this->configureMailer($newsletter);
foreach(array_chunk($queue->subscribers->to_process, self::batch_size) as
$subscribers_ids) {
$subscribers = Subscriber::whereIn('id', $subscribers_ids)
->findArray(); ->findArray();
foreach($db_subscribers as $db_subscriber) { $queue->subscribers = call_user_func_array(
$this->checkExecutionTimer(); array(
$result = $this->sendNewsletter( $this,
$this->processing_method
),
array(
$mailer, $mailer,
$this->processNewsletter($newsletter), $newsletter,
$db_subscriber); $subscribers,
if($result) { $queue
$this->updateStatistics($newsletter['id'], $db_subscriber['id'], $queue->id); )
$subscribers['processed'][] = $db_subscriber['id']; );
} else {
$subscribers['failed'][] = $db_subscriber['id'];
}
$this->updateQueue($queue, $subscribers);
}
} }
} }
} }
function processNewsletter($newsletter) { function processBulkSubscribers($mailer, $newsletter, $subscribers, $queue) {
// TODO: replace shortcodes, etc.. foreach($subscribers as $subscriber) {
$processed_newsletters[] =
$this->processNewsletter($newsletter, $subscriber);
$transformed_subscribers[] =
$mailer->transformSubscriber($subscriber);
}
$result = $this->sendNewsletter(
$mailer,
$processed_newsletters,
$transformed_subscribers
);
$subscribers_ids = Helpers::arrayColumn($subscribers, 'id');
if(!$result) {
$queue->subscribers->failed = array_merge(
$queue->subscribers->failed,
$subscribers_ids
);
} else {
$newsletter_statistics =
array_map(function ($data) use ($newsletter, $subscribers_ids, $queue) {
return array(
$newsletter['id'],
$subscribers_ids[$data],
$queue->id
);
}, range(0, count($transformed_subscribers) - 1));
$newsletter_statistics = Helpers::flattenArray($newsletter_statistics);
$this->updateMailerLog();
$this->updateNewsletterStatistics($newsletter_statistics);
$queue->subscribers->processed = array_merge(
$queue->subscribers->processed,
$subscribers_ids
);
}
$this->updateQueue($queue);
$this->checkSendingLimit();
$this->checkExecutionTimer();
return $queue->subscribers;
}
function processIndividualSubscriber($mailer, $newsletter, $subscribers, $queue) {
foreach($subscribers as $subscriber) {
$this->checkSendingLimit();
$processed_newsletter = $this->processNewsletter($newsletter, $subscriber);
$transformed_subscriber = $mailer->transformSubscriber($subscriber);
$result = $this->sendNewsletter(
$mailer,
$processed_newsletter,
$transformed_subscriber
);
if(!$result) {
$queue->subscribers->failed[] = $subscriber['id'];;
} else {
$queue->subscribers->processed[] = $subscriber['id'];
$newsletter_statistics = array(
$newsletter['id'],
$subscriber['id'],
$queue->id
);
$this->updateMailerLog();
$this->updateNewsletterStatistics($newsletter_statistics);
}
$this->updateQueue($queue);
$this->checkExecutionTimer();
}
return $queue->subscribers;
}
function updateNewsletterStatistics($data) {
return NewsletterStatistics::createMultiple($data);
}
function renderNewsletter($newsletter) {
$renderer = new Renderer($newsletter);
return $renderer->render();
}
function processNewsletter($newsletter, $subscriber = false) {
$divider = '***MailPoet***';
$shortcodes = new Shortcodes(
implode($divider, $newsletter['body']),
$newsletter,
$subscriber
);
list($newsletter['body']['html'], $newsletter['body']['text']) =
explode($divider, $shortcodes->replace());
return $newsletter; return $newsletter;
} }
function sendNewsletter($mailer, $newsletter, $subscriber) { function sendNewsletter($mailer, $newsletter, $subscriber) {
return $mailer->mailer_instance->send( return $mailer->mailer_instance->send(
$newsletter, $newsletter,
$mailer->transformSubscriber($subscriber) $subscriber
); );
} }
function updateStatistics($newsletter_id, $subscriber_id, $queue_id) { function configureMailer($newsletter) {
$newsletter_statistic = NewsletterStatistics::create(); $sender['address'] = (!empty($newsletter['sender_address'])) ?
$newsletter_statistic->subscriber_id = $newsletter_id; $newsletter['sender_address'] :
$newsletter_statistic->newsletter_id = $subscriber_id; false;
$newsletter_statistic->queue_id = $queue_id; $sender['name'] = (!empty($newsletter['sender_name'])) ?
$newsletter_statistic->save(); $newsletter['sender_name'] :
} false;
$reply_to['address'] = (!empty($newsletter['reply_to_address'])) ?
function updateQueue($queue, $subscribers) { $newsletter['reply_to_address'] :
$subscribers['to_process'] = array_values( false;
array_diff( $reply_to['name'] = (!empty($newsletter['reply_to_name'])) ?
$subscribers['to_process'], $newsletter['reply_to_name'] :
array_merge($subscribers['processed'], $subscribers['failed']) false;
) if(!$sender['address']) {
);
$queue->count_processed =
count($subscribers['processed']) + count($subscribers['failed']);
$queue->count_to_process = count($subscribers['to_process']);
$queue->count_failed = count($subscribers['failed']);
$queue->count_total =
$queue->count_processed + $queue->count_to_process;
if(!$queue->count_to_process) {
$queue->processed_at = date('Y-m-d H:i:s');
$queue->status = 'completed';
}
$queue->subscribers = json_encode($subscribers);
$queue->save();
}
function configureMailerForNewsletter($newsletter) {
if(!empty($newsletter['sender_address']) && !empty($newsletter['sender_name'])) {
$sender = array(
'name' => $newsletter['sender_name'],
'address' => $newsletter['sender_address']
);
} else {
$sender = false; $sender = false;
} }
if(!empty($newsletter['reply_to_address']) && !empty($newsletter['reply_to_name'])) { if(!$reply_to['address']) {
$reply_to = array(
'name' => $newsletter['reply_to_name'],
'address' => $newsletter['reply_to_address']
);
} else {
$reply_to = false; $reply_to = false;
} }
$mailer = new Mailer($method = false, $sender, $reply_to); $mailer = new Mailer($method = false, $sender, $reply_to);
return $mailer; return $mailer;
} }
function checkExecutionTimer() {
$elapsed_time = microtime(true) - $this->timer;
if($elapsed_time >= 30) throw new \Exception('Maximum execution time reached.');
}
function getQueues() { function getQueues() {
return \MailPoet\Models\SendingQueue::orderByDesc('priority') return \MailPoet\Models\SendingQueue::orderByDesc('priority')
->whereNull('deleted_at') ->whereNull('deleted_at')
@ -125,10 +192,78 @@ class SendingQueue {
->findResultSet(); ->findResultSet();
} }
function renderNewsletter($newsletter) { function updateQueue($queue) {
$renderer = new Renderer(json_decode($newsletter['body'], true)); $queue = clone($queue);
// TODO: update once text rendering is implemented/enderer returns an array $queue->subscribers->to_process = array_diff(
$newsletter['body'] = array('html' => $renderer->render(), 'text' => ''); $queue->subscribers->to_process,
return $newsletter; array_merge(
$queue->subscribers->processed,
$queue->subscribers->failed
)
);
$queue->subscribers->to_process = array_values($queue->subscribers->to_process);
$queue->count_processed =
count($queue->subscribers->processed) + count($queue->subscribers->failed);
$queue->count_to_process = count($queue->subscribers->to_process);
$queue->count_failed = count($queue->subscribers->failed);
$queue->count_total =
$queue->count_processed + $queue->count_to_process;
if(!$queue->count_to_process) {
$queue->processed_at = date('Y-m-d H:i:s');
$queue->status = 'completed';
}
$queue->subscribers = serialize((array) $queue->subscribers);
$queue->save();
}
function updateMailerLog() {
$this->mta_log['sent']++;
return Setting::setValue('mta_log', $this->mta_log);
}
function getMailerConfig() {
$mta_config = Setting::getValue('mta');
if(!$mta_config) {
throw new \Exception(__('Mailer is not configured.'));
}
return $mta_config;
}
function getMailerLog() {
$mta_log = Setting::getValue('mta_log');
if(!$mta_log) {
$mta_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue('mta_log', $mta_log);
}
return $mta_log;
}
function checkSendingLimit() {
$frequency_interval = (int) $this->mta_config['frequency']['interval'] * 60;
$frequency_limit = (int) $this->mta_config['frequency']['emails'];
$elapsed_time = time() - (int) $this->mta_log['started'];
if($this->mta_log['sent'] === $frequency_limit &&
$elapsed_time <= $frequency_interval
) {
throw new \Exception(__('Sending frequency limit reached.'));
}
if($elapsed_time > $frequency_interval) {
$this->mta_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue('mta_log', $this->mta_log);
}
return;
}
function checkExecutionTimer() {
$elapsed_time = microtime(true) - $this->timer;
if($elapsed_time >= CronHelper::daemon_execution_limit) {
throw new \Exception(__('Maximum execution time reached.'));
}
} }
} }

View File

@ -13,7 +13,9 @@ abstract class Base {
if($block['id'] === 'segments') { if($block['id'] === 'segments') {
$rules['required'] = true; $rules['required'] = true;
$rules['mincheck'] = 1; $rules['mincheck'] = 1;
$rules['error-message'] = __('You need to select a list'); $rules['group'] = $block['id'];
$rules['errors-container'] = '.mailpoet_error_'.$block['id'];
$rules['required-message'] = __('You need to select a list');
} }
if(!empty($block['params']['required'])) { if(!empty($block['params']['required'])) {
@ -29,7 +31,7 @@ abstract class Base {
} }
} }
if($block['type'] === 'radio') { if(in_array($block['type'], array('radio', 'checkbox'))) {
$rules['group'] = 'custom_field_'.$block['id']; $rules['group'] = 'custom_field_'.$block['id'];
$rules['errors-container'] = '.mailpoet_error_'.$block['id']; $rules['errors-container'] = '.mailpoet_error_'.$block['id'];
$rules['required-message'] = __('You need to select at least one option.'); $rules['required-message'] = __('You need to select at least one option.');

View File

@ -9,15 +9,16 @@ class Checkbox extends Base {
$field_name = static::getFieldName($block); $field_name = static::getFieldName($block);
$field_validation = static::getInputValidation($block); $field_validation = static::getInputValidation($block);
// TODO: check if it still makes sense
// create hidden default value
// $html .= '<input type="hidden"name="'.$field_name.'" value="0" '.static::getInputValidation($block).'/>';
$html .= '<p class="mailpoet_paragraph">'; $html .= '<p class="mailpoet_paragraph">';
$html .= static::renderLabel($block); $html .= static::renderLabel($block);
foreach($block['params']['values'] as $option) { $options = (!empty($block['params']['values'])
? $block['params']['values']
: array()
);
foreach($options as $option) {
$html .= '<label class="mailpoet_checkbox_label">'; $html .= '<label class="mailpoet_checkbox_label">';
$html .= '<input type="checkbox" class="mailpoet_checkbox" '; $html .= '<input type="checkbox" class="mailpoet_checkbox" ';
@ -35,6 +36,8 @@ class Checkbox extends Base {
$html .= '</label>'; $html .= '</label>';
} }
$html .= '<span class="mailpoet_error_'.$block['id'].'"></span>';
$html .= '</p>'; $html .= '</p>';
return $html; return $html;

View File

@ -13,9 +13,12 @@ class Radio extends Base {
$html .= static::renderLabel($block); $html .= static::renderLabel($block);
$html .= '<span class="mailpoet_error_'.$block['id'].'"></span>'; $options = (!empty($block['params']['values'])
? $block['params']['values']
: array()
);
foreach($block['params']['values'] as $option) { foreach($options as $option) {
$html .= '<label class="mailpoet_radio_label">'; $html .= '<label class="mailpoet_radio_label">';
$html .= '<input type="radio" class="mailpoet_radio" '; $html .= '<input type="radio" class="mailpoet_radio" ';
@ -33,6 +36,8 @@ class Radio extends Base {
$html .= '</label>'; $html .= '</label>';
} }
$html .= '<span class="mailpoet_error_'.$block['id'].'"></span>';
$html .= '</p>'; $html .= '</p>';
return $html; return $html;

View File

@ -13,23 +13,27 @@ class Segment extends Base {
$html .= static::renderLabel($block); $html .= static::renderLabel($block);
if(!empty($block['params']['values'])) { $options = (!empty($block['params']['values'])
// display values ? $block['params']['values']
foreach($block['params']['values'] as $segment) { : array()
if(!isset($segment['id']) || !isset($segment['name'])) continue; );
$is_checked = (isset($segment['is_checked']) && $segment['is_checked']) ? 'checked="checked"' : ''; foreach($options as $option) {
if(!isset($option['id']) || !isset($option['name'])) continue;
$html .= '<label class="mailpoet_checkbox_label">'; $is_checked = (isset($option['is_checked']) && $option['is_checked']) ? 'checked="checked"' : '';
$html .= '<input type="checkbox" class="mailpoet_checkbox" ';
$html .= 'name="'.$field_name.'[]" '; $html .= '<label class="mailpoet_checkbox_label">';
$html .= 'value="'.$segment['id'].'" '.$is_checked.' '; $html .= '<input type="checkbox" class="mailpoet_checkbox" ';
$html .= $field_validation; $html .= 'name="'.$field_name.'[]" ';
$html .= ' />'.$segment['name']; $html .= 'value="'.$option['id'].'" '.$is_checked.' ';
$html .= '</label>'; $html .= $field_validation;
} $html .= ' />'.$option['name'];
$html .= '</label>';
} }
$html .= '<span class="mailpoet_error_'.$block['id'].'"></span>';
$html .= '</p>'; $html .= '</p>';
return $html; return $html;

View File

@ -6,11 +6,11 @@ class Submit extends Base {
static function render($block) { static function render($block) {
$html = ''; $html = '';
$html .= '<input class="mailpoet_submit" type="submit" '; $html .= '<p class="mailpoet_submit"><input type="submit" ';
$html .= 'value="'.static::getFieldLabel($block).'" '; $html .= 'value="'.static::getFieldLabel($block).'" ';
$html .= '/>'; $html .= '/></p>';
return $html; return $html;
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace MailPoet\Form\Block; namespace MailPoet\Form\Block;
class Input extends Base { class Text extends Base {
static function render($block) { static function render($block) {
$type = 'text'; $type = 'text';
@ -15,7 +15,7 @@ class Input extends Base {
$html .= static::renderLabel($block); $html .= static::renderLabel($block);
$html .= '<input type="'.$type.'" class="mailpoet_input" '; $html .= '<input type="'.$type.'" class="mailpoet_text" ';
$html .= 'name="'.static::getFieldName($block).'" '; $html .= 'name="'.static::getFieldName($block).'" ';

View File

@ -13,9 +13,11 @@ class Renderer {
return $html; return $html;
} }
static function renderStyles($form = array()) { static function renderStyles($form = array(), $prefix = null) {
$styles = new Util\Styles(static::getStyles($form));
$html = '<style type="text/css">'; $html = '<style type="text/css">';
$html .= static::getStyles($form); $html .= $styles->render($prefix);
$html .= '</style>'; $html .= '</style>';
return $html; return $html;
@ -49,7 +51,7 @@ class Renderer {
private static function renderBlock($block = array()) { private static function renderBlock($block = array()) {
$html = ''; $html = '';
switch ($block['type']) { switch($block['type']) {
case 'html': case 'html':
$html .= Block\Html::render($block); $html .= Block\Html::render($block);
break; break;
@ -78,8 +80,8 @@ class Renderer {
$html .= Block\Select::render($block); $html .= Block\Select::render($block);
break; break;
case 'input': case 'text':
$html .= Block\Input::render($block); $html .= Block\Text::render($block);
break; break;
case 'textarea': case 'textarea':

View File

@ -101,7 +101,7 @@ EOL;
} }
private function stripComments($stylesheet) { private function stripComments($stylesheet) {
// remove comments // remove comments
return preg_replace('!/\*.*?\*/!s', '', $stylesheet); return preg_replace('!/\*.*?\*/!s', '', $stylesheet);
} }
@ -111,7 +111,6 @@ EOL;
private function setStyles($styles) { private function setStyles($styles) {
$this->_styles = $styles; $this->_styles = $styles;
return $this; return $this;
} }

View File

@ -65,6 +65,8 @@ class Widget extends \WP_Widget {
) )
); );
$form_edit_url = admin_url('admin.php?page=mailpoet-form-editor&id=');
// set title // set title
$title = isset($instance['title']) ? strip_tags($instance['title']) : ''; $title = isset($instance['title']) ? strip_tags($instance['title']) : '';
@ -102,8 +104,9 @@ class Widget extends \WP_Widget {
endpoint: 'forms', endpoint: 'forms',
action: 'create' action: 'create'
}).done(function(response) { }).done(function(response) {
if(response !== false) { if(response.result && response.form_id) {
window.location = response; window.location =
"<?php echo $form_edit_url; ?>" + response.form_id;
} }
}); });
return false; return false;
@ -151,12 +154,14 @@ class Widget extends \WP_Widget {
$output = ''; $output = '';
if(!empty($body)) { if(!empty($body)) {
$form_id = $this->id_base.'_'.$this->number;
$data = array( $data = array(
'form_id' => $this->id_base.'_'.$this->number, 'form_id' => $form_id,
'form_type' => $form_type, 'form_type' => $form_type,
'form' => $form, 'form' => $form,
'title' => $title, 'title' => $title,
'styles' => FormRenderer::renderStyles($form), 'styles' => FormRenderer::renderStyles($form, '#'.$form_id),
'html' => FormRenderer::renderHTML($form), 'html' => FormRenderer::renderHTML($form),
'before_widget' => (!empty($before_widget) ? $before_widget : ''), 'before_widget' => (!empty($before_widget) ? $before_widget : ''),
'after_widget' => (!empty($after_widget) ? $after_widget : ''), 'after_widget' => (!empty($after_widget) ? $after_widget : ''),

View File

@ -19,82 +19,77 @@ class Mailer {
$this->reply_to = $this->getReplyTo($reply_to); $this->reply_to = $this->getReplyTo($reply_to);
$this->mailer_instance = $this->buildMailer(); $this->mailer_instance = $this->buildMailer();
} }
function send($newsletter, $subscriber) { function send($newsletter, $subscriber) {
$subscriber = $this->transformSubscriber($subscriber); $subscriber = $this->transformSubscriber($subscriber);
return $this->mailer_instance->send($newsletter, $subscriber); return $this->mailer_instance->send($newsletter, $subscriber);
} }
function buildMailer() { function buildMailer() {
switch($this->mailer['method']) { switch($this->mailer['method']) {
case 'AmazonSES': case 'AmazonSES':
$mailer_instance = new $this->mailer['class']( $mailer_instance = new $this->mailer['class'](
$this->mailer['region'], $this->mailer['region'],
$this->mailer['access_key'], $this->mailer['access_key'],
$this->mailer['secret_key'], $this->mailer['secret_key'],
$this->sender['from_name_email'] $this->sender,
); $this->reply_to
break; );
case 'ElasticEmail': break;
$mailer_instance = new $this->mailer['class']( case 'ElasticEmail':
$this->mailer['api_key'], $mailer_instance = new $this->mailer['class'](
$this->sender['from_email'], $this->mailer['api_key'],
$this->sender['from_name'] $this->sender,
); $this->reply_to
break; );
case 'MailGun': break;
$mailer_instance = new $this->mailer['class']( case 'MailGun':
$this->mailer['domain'], $mailer_instance = new $this->mailer['class'](
$this->mailer['api_key'], $this->mailer['domain'],
$this->sender['from_name_email'] $this->mailer['api_key'],
); $this->sender,
break; $this->reply_to
case 'MailPoet': );
$mailer_instance = new $this->mailer['class']( break;
$this->mailer['mailpoet_api_key'], case 'MailPoet':
$this->sender['from_email'], $mailer_instance = new $this->mailer['class'](
$this->sender['from_name'] $this->mailer['mailpoet_api_key'],
); $this->sender,
break; $this->reply_to
case 'Mandrill': );
$mailer_instance = new $this->mailer['class']( break;
$this->mailer['api_key'], case 'SendGrid':
$this->sender['from_email'], $mailer_instance = new $this->mailer['class'](
$this->sender['from_name'] $this->mailer['api_key'],
); $this->sender,
break; $this->reply_to
case 'SendGrid': );
$mailer_instance = new $this->mailer['class']( break;
$this->mailer['api_key'], case 'PHPMail':
$this->sender['from_email'], $mailer_instance = new $this->mailer['class'](
$this->sender['from_name'] $this->sender,
); $this->reply_to
break; );
case 'WPMail': break;
$mailer_instance = new $this->mailer['class']( case 'SMTP':
$this->sender['from_email'], $mailer_instance = new $this->mailer['class'](
$this->sender['from_name'] $this->mailer['host'],
); $this->mailer['port'],
break; $this->mailer['authentication'],
case 'SMTP': $this->mailer['login'],
$mailer_instance = new $this->mailer['class']( $this->mailer['password'],
$this->mailer['host'], $this->mailer['encryption'],
$this->mailer['port'], $this->sender,
$this->mailer['authentication'], $this->reply_to
$this->mailer['login'], );
$this->mailer['password'], break;
$this->mailer['encryption'], default:
$this->sender['from_email'], throw new \Exception(__('Mailing method does not exist.'));
$this->sender['from_name'] break;
);
break;
default:
throw new \Exception(__('Mailing method does not exist.'));
break;
} }
return $mailer_instance; return $mailer_instance;
} }
function getMailer($mailer = false) { function getMailer($mailer = false) {
if(!$mailer) { if(!$mailer) {
$mailer = Setting::getValue('mta', null); $mailer = Setting::getValue('mta', null);
@ -107,7 +102,7 @@ class Mailer {
function getSender($sender = false) { function getSender($sender = false) {
if(!$sender) { if(!$sender) {
$sender = Setting::getValue('sender', null); $sender = Setting::getValue('sender', null);
if(!$sender) throw new \Exception(__('Sender name and email are not configured.')); if(!$sender['address']) throw new \Exception(__('Sender name and email are not configured.'));
} }
return array( return array(
'from_name' => $sender['name'], 'from_name' => $sender['name'],
@ -126,6 +121,9 @@ class Mailer {
); );
} }
} }
if(!$reply_to['address']) {
$reply_to['address'] = $this->sender['from_email'];
}
return array( return array(
'reply_to_name' => $reply_to['name'], 'reply_to_name' => $reply_to['name'],
'reply_to_email' => $reply_to['address'], 'reply_to_email' => $reply_to['address'],

View File

@ -13,25 +13,27 @@ class AmazonSES {
public $aws_termination_string; public $aws_termination_string;
public $hash_algorithm; public $hash_algorithm;
public $url; public $url;
public $from; public $sender;
public $reply_to;
public $date; public $date;
public $date_without_time; public $date_without_time;
function __construct($region, $access_key, $secret_key, $from) { function __construct($region, $access_key, $secret_key, $sender, $reply_to) {
$this->aws_access_key = $access_key; $this->aws_access_key = $access_key;
$this->aws_secret_key = $secret_key; $this->aws_secret_key = $secret_key;
$this->aws_region = $region; $this->aws_region = $region;
$this->aws_endpoint = sprintf('email.%s.amazonaws.com', $region); $this->aws_endpoint = sprintf('email.%s.amazonaws.com', $this->aws_region);
$this->aws_signing_algorithm = 'AWS4-HMAC-SHA256'; $this->aws_signing_algorithm = 'AWS4-HMAC-SHA256';
$this->aws_service = 'ses'; $this->aws_service = 'ses';
$this->aws_termination_string = 'aws4_request'; $this->aws_termination_string = 'aws4_request';
$this->hash_algorithm = 'sha256'; $this->hash_algorithm = 'sha256';
$this->url = 'https://' . $this->aws_endpoint; $this->url = 'https://' . $this->aws_endpoint;
$this->from = $from; $this->sender = $sender;
$this->reply_to = $reply_to;
$this->date = gmdate('Ymd\THis\Z'); $this->date = gmdate('Ymd\THis\Z');
$this->date_without_time = gmdate('Ymd'); $this->date_without_time = gmdate('Ymd');
} }
function send($newsletter, $subscriber) { function send($newsletter, $subscriber) {
$result = wp_remote_post( $result = wp_remote_post(
$this->url, $this->url,
@ -42,15 +44,16 @@ class AmazonSES {
wp_remote_retrieve_response_code($result) === 200 wp_remote_retrieve_response_code($result) === 200
); );
} }
function getBody($newsletter, $subscriber) { function getBody($newsletter, $subscriber) {
$body = array( $body = array(
'Action' => 'SendEmail', 'Action' => 'SendEmail',
'Version' => '2010-12-01', 'Version' => '2010-12-01',
'Source' => $this->from,
'Destination.ToAddresses.member.1' => $subscriber, 'Destination.ToAddresses.member.1' => $subscriber,
'Source' => $this->sender['from_name_email'],
'ReplyToAddresses.member.1' => $this->reply_to['reply_to_name_email'],
'Message.Subject.Data' => $newsletter['subject'], 'Message.Subject.Data' => $newsletter['subject'],
'ReturnPath' => $this->from 'ReturnPath' => $this->sender['from_name_email'],
); );
if(!empty($newsletter['body']['html'])) { if(!empty($newsletter['body']['html'])) {
$body['Message.Body.Html.Data'] = $newsletter['body']['html']; $body['Message.Body.Html.Data'] = $newsletter['body']['html'];
@ -60,7 +63,7 @@ class AmazonSES {
} }
return $body; return $body;
} }
function request($newsletter, $subscriber) { function request($newsletter, $subscriber) {
$body = $this->getBody($newsletter, $subscriber); $body = $this->getBody($newsletter, $subscriber);
return array( return array(
@ -75,7 +78,7 @@ class AmazonSES {
'body' => urldecode(http_build_query($body)) 'body' => urldecode(http_build_query($body))
); );
} }
function signRequest($body) { function signRequest($body) {
$string_to_sign = $this->createStringToSign( $string_to_sign = $this->createStringToSign(
$this->getCredentialScope(), $this->getCredentialScope(),
@ -86,7 +89,7 @@ class AmazonSES {
$string_to_sign, $string_to_sign,
$this->getSigningKey() $this->getSigningKey()
); );
return sprintf( return sprintf(
'%s Credential=%s/%s, SignedHeaders=host;x-amz-date, Signature=%s', '%s Credential=%s/%s, SignedHeaders=host;x-amz-date, Signature=%s',
$this->aws_signing_algorithm, $this->aws_signing_algorithm,
@ -94,7 +97,7 @@ class AmazonSES {
$this->getCredentialScope(), $this->getCredentialScope(),
$signature); $signature);
} }
function getCredentialScope() { function getCredentialScope() {
return sprintf( return sprintf(
'%s/%s/%s/%s', '%s/%s/%s/%s',
@ -103,7 +106,7 @@ class AmazonSES {
$this->aws_service, $this->aws_service,
$this->aws_termination_string); $this->aws_termination_string);
} }
function getCanonicalRequest($body) { function getCanonicalRequest($body) {
return implode("\n", array( return implode("\n", array(
'POST', 'POST',
@ -116,7 +119,7 @@ class AmazonSES {
hash($this->hash_algorithm, urldecode(http_build_query($body))) hash($this->hash_algorithm, urldecode(http_build_query($body)))
)); ));
} }
function createStringToSign($credential_scope, $canonical_request) { function createStringToSign($credential_scope, $canonical_request) {
return implode("\n", array( return implode("\n", array(
$this->aws_signing_algorithm, $this->aws_signing_algorithm,
@ -125,7 +128,7 @@ class AmazonSES {
hash($this->hash_algorithm, $canonical_request) hash($this->hash_algorithm, $canonical_request)
)); ));
} }
function getSigningKey() { function getSigningKey() {
$date_key = hash_hmac( $date_key = hash_hmac(
$this->hash_algorithm, $this->hash_algorithm,

View File

@ -6,13 +6,13 @@ if(!defined('ABSPATH')) exit;
class ElasticEmail { class ElasticEmail {
public $url = 'https://api.elasticemail.com/mailer/send'; public $url = 'https://api.elasticemail.com/mailer/send';
public $api_key; public $api_key;
public $from_email; public $sender;
public $from_name; public $reply_to;
function __construct($api_key, $from_email, $from_name) { function __construct($api_key, $sender, $reply_to) {
$this->api_key = $api_key; $this->api_key = $api_key;
$this->from_email = $from_email; $this->sender = $sender;
$this->from_name = $from_name; $this->reply_to = $reply_to;
} }
function send($newsletter, $subscriber) { function send($newsletter, $subscriber) {
@ -28,9 +28,11 @@ class ElasticEmail {
function getBody($newsletter, $subscriber) { function getBody($newsletter, $subscriber) {
$body = array( $body = array(
'api_key' => $this->api_key, 'api_key' => $this->api_key,
'from' => $this->from_email,
'from_name' => $this->from_name,
'to' => $subscriber, 'to' => $subscriber,
'from' => $this->sender['from_email'],
'from_name' => $this->sender['from_name'],
'reply_to' => $this->reply_to['reply_to_email'],
'reply_to_name' => $this->reply_to['reply_to_name'],
'subject' => $newsletter['subject'] 'subject' => $newsletter['subject']
); );
if(!empty($newsletter['body']['html'])) { if(!empty($newsletter['body']['html'])) {

View File

@ -6,12 +6,14 @@ if(!defined('ABSPATH')) exit;
class MailGun { class MailGun {
public $url; public $url;
public $api_key; public $api_key;
public $from; public $sender;
public $reply_to;
function __construct($domain, $api_key, $from) {
function __construct($domain, $api_key, $sender, $reply_to) {
$this->url = sprintf('https://api.mailgun.net/v3/%s/messages', $domain); $this->url = sprintf('https://api.mailgun.net/v3/%s/messages', $domain);
$this->api_key = $api_key; $this->api_key = $api_key;
$this->from = $from; $this->sender = $sender;
$this->reply_to = $reply_to;
} }
function send($newsletter, $subscriber) { function send($newsletter, $subscriber) {
@ -27,8 +29,9 @@ class MailGun {
function getBody($newsletter, $subscriber) { function getBody($newsletter, $subscriber) {
$body = array( $body = array(
'from' => $this->from,
'to' => $subscriber, 'to' => $subscriber,
'from' => $this->sender['from_name_email'],
'h:Reply-To' => $this->reply_to['reply_to_name_email'],
'subject' => $newsletter['subject'] 'subject' => $newsletter['subject']
); );
if(!empty($newsletter['body']['html'])) { if(!empty($newsletter['body']['html'])) {

View File

@ -6,26 +6,27 @@ if(!defined('ABSPATH')) exit;
class MailPoet { class MailPoet {
public $url = 'https://bridge.mailpoet.com/api/messages'; public $url = 'https://bridge.mailpoet.com/api/messages';
public $api_key; public $api_key;
public $from_email; public $sender;
public $from_name; public $reply_to;
function __construct($api_key, $from_email, $from_name) { function __construct($api_key, $sender, $reply_to) {
$this->api_key = $api_key; $this->api_key = $api_key;
$this->from_email = $from_email; $this->sender = $sender;
$this->from_name = $from_name; $this->reply_to = $reply_to;
} }
function send($newsletter, $subscriber) { function send($newsletter, $subscriber) {
$message_body = $this->getBody($newsletter, $subscriber);
$result = wp_remote_post( $result = wp_remote_post(
$this->url, $this->url,
$this->request($newsletter, $this->processSubscriber($subscriber)) $this->request($message_body)
); );
return ( return (
!is_wp_error($result) === true && !is_wp_error($result) === true &&
wp_remote_retrieve_response_code($result) === 201 wp_remote_retrieve_response_code($result) === 201
); );
} }
function processSubscriber($subscriber) { function processSubscriber($subscriber) {
preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriber, $subscriber_data); preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriber, $subscriber_data);
if(!isset($subscriber_data['email'])) { if(!isset($subscriber_data['email'])) {
@ -38,34 +39,51 @@ class MailPoet {
'name' => (isset($subscriber_data['name'])) ? $subscriber_data['name'] : '' 'name' => (isset($subscriber_data['name'])) ? $subscriber_data['name'] : ''
); );
} }
function getBody($newsletter, $subscriber) { function getBody($newsletter, $subscriber) {
$body = array( $composeBody = function ($newsletter, $subscriber) {
'to' => (array( $body = array(
'address' => $subscriber['email'], 'to' => (array(
'name' => $subscriber['name'] 'address' => $subscriber['email'],
)), 'name' => $subscriber['name']
'from' => (array( )),
'address' => $this->from_email, 'from' => (array(
'name' => $this->from_name 'address' => $this->sender['from_email'],
)), 'name' => $this->sender['from_name']
'subject' => $newsletter['subject'] )),
); 'reply_to' => (array(
if(!empty($newsletter['body']['html'])) { 'address' => $this->reply_to['reply_to_email'],
$body['html'] = $newsletter['body']['html']; 'name' => $this->reply_to['reply_to_name']
} )),
if(!empty($newsletter['body']['text'])) { 'subject' => $newsletter['subject']
$body['text'] = $newsletter['body']['text']; );
if(!empty($newsletter['body']['html'])) {
$body['html'] = $newsletter['body']['html'];
}
if(!empty($newsletter['body']['text'])) {
$body['text'] = $newsletter['body']['text'];
}
return $body;
};
if(is_array($newsletter) && is_array($subscriber)) {
$body = array();
for($record = 0; $record < count($newsletter); $record++) {
$body[] = $composeBody(
$newsletter[$record],
$this->processSubscriber($subscriber[$record])
);
}
} else {
$body[] = $composeBody($newsletter, $this->processSubscriber($subscriber));
} }
return $body; return $body;
} }
function auth() { function auth() {
return 'Basic ' . base64_encode('api:' . $this->api_key); return 'Basic ' . base64_encode('api:' . $this->api_key);
} }
function request($newsletter, $subscriber) { function request($body) {
$body = array($this->getBody($newsletter, $subscriber));
return array( return array(
'timeout' => 10, 'timeout' => 10,
'httpversion' => '1.0', 'httpversion' => '1.0',
@ -74,7 +92,7 @@ class MailPoet {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'Authorization' => $this->auth() 'Authorization' => $this->auth()
), ),
'body' => $body 'body' => json_encode($body)
); );
} }
} }

View File

@ -1,75 +0,0 @@
<?php
namespace MailPoet\Mailer\Methods;
if(!defined('ABSPATH')) exit;
class Mandrill {
public $url = 'https://mandrillapp.com/api/1.0/messages/send.json';
public $api_key;
public $from_email;
public $from_name;
function __construct($api_key, $from_email, $from_name) {
$this->api_key = $api_key;
$this->from_name = $from_name;
$this->from_email = $from_email;
}
function send($newsletter, $subscriber) {
$result = wp_remote_post(
$this->url,
$this->request($newsletter, $this->processSubscriber($subscriber))
);
return (
!is_wp_error($result) === true &&
!preg_match('!invalid!', $result['body']) === true &&
wp_remote_retrieve_response_code($result) === 200
);
}
function processSubscriber($subscriber) {
preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriber, $subscriber_data);
if(!isset($subscriber_data['email'])) {
$subscriber_data = array(
'email' => $subscriber,
);
}
return array(
'email' => $subscriber_data['email'],
'name' => (isset($subscriber_data['name'])) ? $subscriber_data['name'] : ''
);
}
function getBody($newsletter, $subscriber) {
$body = array(
'key' => $this->api_key,
'message' => array(
'from_email' => $this->from_email,
'from_name' => $this->from_name,
'to' => array($subscriber),
'subject' => $newsletter['subject']
),
'async' => false,
);
if(!empty($newsletter['body']['html'])) {
$body['message']['html'] = $newsletter['body']['html'];
}
if(!empty($newsletter['body']['text'])) {
$body['message']['text'] = $newsletter['body']['text'];
}
return $body;
}
function request($newsletter, $subscriber) {
$body = $this->getBody($newsletter, $subscriber);
return array(
'timeout' => 10,
'httpversion' => '1.0',
'method' => 'POST',
'headers' => array(
'Content-Type' => 'application/json'
),
'body' => json_encode($body)
);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace MailPoet\Mailer\Methods;
if(!defined('ABSPATH')) exit;
class PHPMail {
public $sender;
public $reply_to;
public $mailer;
function __construct($sender, $reply_to) {
$this->sender = $sender;
$this->reply_to = $reply_to;
$this->mailer = $this->buildMailer();
}
function send($newsletter, $subscriber) {
try {
$message = $this->createMessage($newsletter, $subscriber);
$result = $this->mailer->send($message);
} catch(\Exception $e) {
$result = false;
}
return ($result === 1);
}
function buildMailer() {
$transport = \Swift_SmtpTransport::newInstance();
$transport->setTimeout(10);
return \Swift_Mailer::newInstance($transport);
}
function createMessage($newsletter, $subscriber) {
$message = \Swift_Message::newInstance()
->setTo($this->processSubscriber($subscriber))
->setFrom(array(
$this->sender['from_email'] => $this->sender['from_name']
))
->setReplyTo(array(
$this->reply_to['reply_to_email'] => $this->reply_to['reply_to_name']
))
->setSubject($newsletter['subject']);
if(!empty($newsletter['body']['html'])) {
$message = $message->setBody($newsletter['body']['html'], 'text/html');
}
if(!empty($newsletter['body']['text'])) {
$message = $message->addPart($newsletter['body']['text'], 'text/plain');
}
return $message;
}
function processSubscriber($subscriber) {
preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriber, $subscriber_data);
if(!isset($subscriber_data['email'])) {
$subscriber_data = array(
'email' => $subscriber,
);
}
return array(
$subscriber_data['email'] =>
(isset($subscriber_data['name'])) ? $subscriber_data['name'] : ''
);
}
}

View File

@ -10,21 +10,21 @@ class SMTP {
public $login; public $login;
public $password; public $password;
public $encryption; public $encryption;
public $from_name; public $sender;
public $from_email; public $reply_to;
public $mailer; public $mailer;
function __construct( function __construct(
$host, $port, $authentication, $login = null, $password = null, $encryption, $host, $port, $authentication, $login = null, $password = null, $encryption,
$from_email, $from_name) { $sender, $reply_to) {
$this->host = $host; $this->host = $host;
$this->port = $port; $this->port = $port;
$this->authentication = $authentication; $this->authentication = $authentication;
$this->login = $login; $this->login = $login;
$this->password = $password; $this->password = $password;
$this->encryption = $encryption; $this->encryption = $encryption;
$this->from_name = $from_name; $this->sender = $sender;
$this->from_email = $from_email; $this->reply_to = $reply_to;
$this->mailer = $this->buildMailer(); $this->mailer = $this->buildMailer();
} }
@ -50,11 +50,15 @@ class SMTP {
return \Swift_Mailer::newInstance($transport); return \Swift_Mailer::newInstance($transport);
} }
function createMessage($newsletter, $subscriber) { function createMessage($newsletter, $subscriber) {
$message = \Swift_Message::newInstance() $message = \Swift_Message::newInstance()
->setFrom(array($this->from_email => $this->from_name))
->setTo($this->processSubscriber($subscriber)) ->setTo($this->processSubscriber($subscriber))
->setFrom(array(
$this->sender['from_email'] => $this->sender['from_name']
))
->setReplyTo(array(
$this->reply_to['reply_to_email'] => $this->reply_to['reply_to_name']
))
->setSubject($newsletter['subject']); ->setSubject($newsletter['subject']);
if(!empty($newsletter['body']['html'])) { if(!empty($newsletter['body']['html'])) {
$message = $message->setBody($newsletter['body']['html'], 'text/html'); $message = $message->setBody($newsletter['body']['html'], 'text/html');

View File

@ -6,13 +6,13 @@ if(!defined('ABSPATH')) exit;
class SendGrid { class SendGrid {
public $url = 'https://api.sendgrid.com/api/mail.send.json'; public $url = 'https://api.sendgrid.com/api/mail.send.json';
public $api_key; public $api_key;
public $from_email; public $sender;
public $from_name; public $reply_to;
function __construct($api_key, $from_email, $from_name) { function __construct($api_key, $sender, $reply_to) {
$this->api_key = $api_key; $this->api_key = $api_key;
$this->from_email = $from_email; $this->sender = $sender;
$this->from_name = $from_name; $this->reply_to = $reply_to;
} }
function send($newsletter, $subscriber) { function send($newsletter, $subscriber) {
@ -20,10 +20,11 @@ class SendGrid {
$this->url, $this->url,
$this->request($newsletter, $subscriber) $this->request($newsletter, $subscriber)
); );
$result_body = json_decode($result['body'], true);
return ( return (
!is_wp_error($result) === true && !is_wp_error($result) === true &&
!preg_match('!invalid!', $result['body']) === true && !preg_match('!invalid!', $result['body']) === true &&
!isset(json_decode($result['body'], true)['errors']) === true && !isset($result_body['errors']) === true &&
wp_remote_retrieve_response_code($result) === 200 wp_remote_retrieve_response_code($result) === 200
); );
} }
@ -31,8 +32,9 @@ class SendGrid {
function getBody($newsletter, $subscriber) { function getBody($newsletter, $subscriber) {
$body = array( $body = array(
'to' => $subscriber, 'to' => $subscriber,
'from' => $this->from_email, 'from' => $this->sender['from_email'],
'from_name' => $this->from_name, 'fromname' => $this->sender['from_name'],
'replyto' => $this->reply_to['reply_to_email'],
'subject' => $newsletter['subject'] 'subject' => $newsletter['subject']
); );
if(!empty($newsletter['body']['html'])) { if(!empty($newsletter['body']['html'])) {
@ -57,7 +59,7 @@ class SendGrid {
'headers' => array( 'headers' => array(
'Authorization' => $this->auth() 'Authorization' => $this->auth()
), ),
'body' => urldecode(http_build_query($body)) 'body' => http_build_query($body)
); );
} }
} }

View File

@ -1,63 +0,0 @@
<?php
namespace MailPoet\Mailer\Methods;
require_once(ABSPATH . 'wp-includes/pluggable.php');
if(!defined('ABSPATH')) exit;
class WPMail {
public $from_email;
public $from_name;
public $filters = array(
'wp_mail_from' => 'setFromEmail',
'wp_mail_from_name' => 'setFromName',
'wp_mail_content_type' => 'setContentType'
);
function __construct($from_email, $from_name) {
$this->from_email = $from_email;
$this->from_name = $from_name;
}
function addFilters() {
foreach($this->filters as $filter => $method) {
add_filter($filter, array(
$this,
$method
));
}
}
function removeFilters() {
foreach($this->filters as $filter => $method) {
remove_filter($filter, array(
$this,
$method
));
}
}
function setFromEmail() {
return $this->from_email;
}
function setFromName() {
return $this->from_name;
}
function setContentType() {
return 'text/html';
}
function send($newsletter, $subscriber) {
$this->addFilters();
$result = wp_mail(
$subscriber, $newsletter['subject'],
(!empty($newsletter['body']['html'])) ?
$newsletter['body']['html'] :
$newsletter['body']['text']
);
$this->removeFilters();
return ($result === true);
}
}

View File

@ -19,20 +19,22 @@ class CustomField extends Model {
function asArray() { function asArray() {
$model = parent::asArray(); $model = parent::asArray();
$model['params'] = ( if(isset($model['params'])) {
is_serialized($this->params) $model['params'] = (is_array($this->params))
? unserialize($this->params) ? $this->params
: $this->params : unserialize($this->params);
); }
return $model; return $model;
} }
function save() { function save() {
if(is_null($this->params)) {
$this->params = array();
}
$this->set('params', ( $this->set('params', (
is_serialized($this->params) is_array($this->params)
? $this->params ? serialize($this->params)
: serialize($this->params) : $this->params
)); ));
return parent::save(); return parent::save();
} }
@ -54,7 +56,7 @@ class CustomField extends Model {
} }
// set name as label by default // set name as label by default
if(empty($data['params']['label'])) { if(empty($data['params']['label']) && isset($data['name'])) {
$data['params']['label'] = $data['name']; $data['params']['label'] = $data['name'];
} }
@ -66,12 +68,6 @@ class CustomField extends Model {
$custom_field->set($data); $custom_field->set($data);
} }
try { return $custom_field->save();
$custom_field->save();
return $custom_field;
} catch(Exception $e) {
return $custom_field->getValidationErrors();
}
return false;
} }
} }

View File

@ -1,16 +0,0 @@
<?php namespace MailPoet\Models;
class CustomValidator {
function __construct() {
$this->validator = new \Sudzy\Engine();
}
function init() {
$this->validator
->addValidator('isString', function ($val) {
return is_string($val);
});
return $this->validator;
}
}

View File

@ -1,50 +0,0 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class Filter extends Model {
static $_table = MP_FILTERS_TABLE;
function __construct() {
parent::__construct();
$this->addValidations('name', array(
'required' => __('You need to specify a name.')
));
}
function delete() {
// delete all relations to subscribers
SegmentFilter::where('filter_id', $this->id)->deleteMany();
return parent::delete();
}
function segments() {
return $this->has_many_through(
__NAMESPACE__.'\Segment',
__NAMESPACE__.'\SegmentFilter',
'filter_id',
'segment_id'
);
}
static function createOrUpdate($data = array()) {
$filter = false;
if(isset($data['id']) && (int)$data['id'] > 0) {
$filter = self::findOne((int)$data['id']);
}
if($filter === false) {
$filter = self::create();
$filter->hydrate($data);
} else {
unset($data['id']);
$filter->set($data);
}
$filter->save();
return $filter;
}
}

View File

@ -17,36 +17,30 @@ class Form extends Model {
function asArray() { function asArray() {
$model = parent::asArray(); $model = parent::asArray();
$model['body'] = ( $model['body'] = (is_serialized($this->body))
is_serialized($this->body)
? unserialize($this->body) ? unserialize($this->body)
: $this->body : $this->body;
); $model['settings'] = (is_serialized($this->settings))
$model['settings'] = (
is_serialized($this->settings)
? unserialize($this->settings) ? unserialize($this->settings)
: $this->settings : $this->settings;
);
return $model; return $model;
} }
function save() { function save() {
$this->set('body', ( $this->set('body', (is_serialized($this->body))
is_serialized($this->body)
? $this->body ? $this->body
: serialize($this->body) : serialize($this->body)
)); );
$this->set('settings', ( $this->set('settings', (is_serialized($this->settings))
is_serialized($this->settings)
? $this->settings ? $this->settings
: serialize($this->settings) : serialize($this->settings)
)); );
return parent::save(); return parent::save();
} }
static function search($orm, $search = '') { static function search($orm, $search = '') {
return $orm->where_like('name', '%'.$search.'%'); return $orm->whereLike('name', '%'.$search.'%');
} }
static function groups() { static function groups() {
@ -67,9 +61,8 @@ class Form extends Model {
static function groupBy($orm, $group = null) { static function groupBy($orm, $group = null) {
if($group === 'trash') { if($group === 'trash') {
return $orm->whereNotNull('deleted_at'); return $orm->whereNotNull('deleted_at');
} else {
$orm = $orm->whereNull('deleted_at');
} }
return $orm->whereNull('deleted_at');
} }
static function createOrUpdate($data = array()) { static function createOrUpdate($data = array()) {
@ -87,7 +80,6 @@ class Form extends Model {
$form->set($data); $form->set($data);
} }
$form->save(); return $form->save();
return $form;
} }
} }

View File

@ -4,25 +4,65 @@ namespace MailPoet\Models;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class Model extends \Sudzy\ValidModel { class Model extends \Sudzy\ValidModel {
protected $_errors;
function __construct() { function __construct() {
$customValidators = new CustomValidator(); $this->_errors = array();
parent::__construct($customValidators->init()); parent::__construct();
} }
static function create() { static function create() {
return parent::create(); return parent::create();
} }
function getErrors() {
if(empty($this->_errors)) {
return false;
} else {
return $this->_errors;
}
}
function setError($error = '') {
if(!empty($error)) {
if(is_array($error)) {
$this->_errors = array_merge($this->_errors, $error);
$this->_errors = array_unique($this->_errors);
} else {
$this->_errors[] = $error;
}
}
}
function save() { function save() {
$this->setTimestamp(); $this->setTimestamp();
try { try {
parent::save(); parent::save();
return true; } catch(\Sudzy\ValidationException $e) {
} catch (\Sudzy\ValidationException $e) { $this->setError($e->getValidationErrors());
return array_unique($e->getValidationErrors()); } catch(\PDOException $e) {
} catch (\PDOException $e) { switch($e->getCode()) {
return $e->getMessage(); case 23000:
preg_match("/for key \'(.*?)\'/i", $e->getMessage(), $matches);
if(isset($matches[1])) {
$column = $matches[1];
$this->setError(
sprintf(
__('Another record already exists. Please specify a different "%1$s".'),
$column
)
);
} else {
$this->setError($e->getMessage());
}
break;
default:
$this->setError($e->getMessage());
}
} catch(\Exception $e) {
$this->setError($e->getMessage());
} }
return $this;
} }
function trash() { function trash() {

View File

@ -8,6 +8,10 @@ class Newsletter extends Model {
function __construct() { function __construct() {
parent::__construct(); parent::__construct();
$this->addValidations('type', array(
'required' => __('You need to specify a type.')
));
} }
function save() { function save() {
@ -15,9 +19,23 @@ class Newsletter extends Model {
$this->set_expr('deleted_at', 'NULL'); $this->set_expr('deleted_at', 'NULL');
} }
$this->set('body',
is_array($this->body)
? json_encode($this->body)
: $this->body
);
return parent::save(); return parent::save();
} }
function asArray() {
$model = parent::asArray();
if(isset($model['body'])) {
$model['body'] = json_decode($model['body'], true);
}
return $model;
}
function delete() { function delete() {
// delete all relations to segments // delete all relations to segments
NewsletterSegment::where('newsletter_id', $this->id)->deleteMany(); NewsletterSegment::where('newsletter_id', $this->id)->deleteMany();
@ -79,14 +97,13 @@ class Newsletter extends Model {
} }
static function filterBy($orm, $filters = null) { static function filterBy($orm, $filters = null) {
if(empty($filters)) { if(!empty($filters)) {
return $orm; foreach($filters as $key => $value) {
} if($key === 'segment') {
foreach($filters as $key => $value) { $segment = Segment::findOne($value);
if($key === 'segment') { if($segment !== false) {
$segment = Segment::findOne($value); $orm = $segment->newsletters();
if($segment !== false) { }
$orm = $segment->newsletters();
} }
} }
} }

View File

@ -9,4 +9,16 @@ class NewsletterStatistics extends Model {
function __construct() { function __construct() {
parent::__construct(); parent::__construct();
} }
static function createMultiple($data) {
return self::rawExecute(
'INSERT INTO `' . NewsletterStatistics::$_table . '` ' .
'(newsletter_id, subscriber_id, queue_id) ' .
'VALUES ' . rtrim(
str_repeat('(?,?,?), ', count($data)/3),
', '
),
$data
);
}
} }

View File

@ -17,6 +17,14 @@ class NewsletterTemplate extends Model {
)); ));
} }
function asArray() {
$template = parent::asArray();
if(isset($template['body'])) {
$template['body'] = json_decode($template['body'], true);
}
return $template;
}
static function createOrUpdate($data = array()) { static function createOrUpdate($data = array()) {
$template = false; $template = false;
@ -32,16 +40,7 @@ class NewsletterTemplate extends Model {
$template->set($data); $template->set($data);
} }
$saved = $template->save(); $template->save();
return $template;
if($saved === true) {
return true;
} else {
$errors = $template->getValidationErrors();
if(!empty($errors)) {
return $errors;
}
}
return false;
} }
} }

View File

@ -122,6 +122,7 @@ class Segment extends Model {
->group_by(self::$_table.'.id') ->group_by(self::$_table.'.id')
->group_by(self::$_table.'.name') ->group_by(self::$_table.'.name')
->where(self::$_table.'.type', 'default') ->where(self::$_table.'.type', 'default')
->whereNull(self::$_table.'.deleted_at')
->findArray(); ->findArray();
} }
@ -164,8 +165,7 @@ class Segment extends Model {
$segment->set($data); $segment->set($data);
} }
$segment->save(); return $segment->save();
return $segment;
} }
static function getPublic() { static function getPublic() {

View File

@ -1,12 +0,0 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class SegmentFilter extends Model {
public static $_table = MP_SEGMENT_FILTER_TABLE;
function __construct() {
parent::__construct();
}
}

View File

@ -15,7 +15,8 @@ class SendingQueue extends Model {
return false; return false;
} else { } else {
$this->set('status', 'paused'); $this->set('status', 'paused');
return $this->save(); $this->save();
return ($this->getErrors() === false && $this->id() > 0);
} }
} }
@ -24,12 +25,14 @@ class SendingQueue extends Model {
return $this->complete(); return $this->complete();
} else { } else {
$this->set_expr('status', 'NULL'); $this->set_expr('status', 'NULL');
return $this->save(); $this->save();
return ($this->getErrors() === false && $this->id() > 0);
} }
} }
function complete() { function complete() {
$this->set('status', 'completed'); $this->set('status', 'completed');
return $this->save(); $this->save();
return ($this->getErrors() === false && $this->id() > 0);
} }
} }

View File

@ -10,8 +10,7 @@ class Setting extends Model {
parent::__construct(); parent::__construct();
$this->addValidations('name', array( $this->addValidations('name', array(
'required' => 'name_is_blank', 'required' => __('You need to specify a name.')
'isString' => 'name_is_not_string'
)); ));
} }
@ -36,6 +35,9 @@ class Setting extends Model {
if($setting !== $default) { if($setting !== $default) {
for($i = 0, $count = count($keys); $i < $count; $i++) { for($i = 0, $count = count($keys); $i < $count; $i++) {
if(!is_array($setting)) {
$setting = array();
}
if(array_key_exists($keys[$i], $setting)) { if(array_key_exists($keys[$i], $setting)) {
$setting = $setting[$keys[$i]]; $setting = $setting[$keys[$i]];
} else { } else {
@ -55,10 +57,11 @@ class Setting extends Model {
$value = serialize($value); $value = serialize($value);
} }
return Setting::createOrUpdate(array( $setting = Setting::createOrUpdate(array(
'name' => $key, 'name' => $key,
'value' => $value 'value' => $value
)); ));
return ($setting->id() > 0 && $setting->getErrors() === false);
} else { } else {
$main_key = array_shift($keys); $main_key = array_shift($keys);
@ -67,13 +70,6 @@ class Setting extends Model {
$last_key = array_pop($keys); $last_key = array_pop($keys);
foreach($keys as $key) { foreach($keys as $key) {
if(!is_array($current_value)) {
$current_value = array();
}
if(!array_key_exists($key, $current_value)) {
$current_value = array($key => array());
}
$current_value =& $current_value[$key]; $current_value =& $current_value[$key];
} }
if(is_scalar($current_value)) { if(is_scalar($current_value)) {
@ -100,17 +96,20 @@ class Setting extends Model {
return $settings; return $settings;
} }
public static function createOrUpdate($model) { public static function createOrUpdate($data = array()) {
$exists = self::where('name', $model['name']) $setting = false;
->find_one();
if($exists === false) { if(isset($data['name'])) {
$new_model = self::create(); $setting = self::where('name', $data['name'])->findOne();
$new_model->hydrate($model);
return $new_model->save();
} }
$exists->value = $model['value']; if($setting === false) {
return $exists->save(); $setting = self::create();
$setting->hydrate($data);
} else {
$setting->value = $data['value'];
}
return $setting->save();
} }
} }

View File

@ -33,12 +33,17 @@ class Subscriber extends Model {
} }
function addToSegments(array $segment_ids = array()) { function addToSegments(array $segment_ids = array()) {
$segments = Segment::whereIn('id', $segment_ids)->findMany(); // delete all relations to segments
foreach($segments as $segment) { SubscriberSegment::where('subscriber_id', $this->id)->deleteMany();
$association = SubscriberSegment::create();
$association->subscriber_id = $this->id; if(!empty($segment_ids)) {
$association->segment_id = $segment->id; $segments = Segment::whereIn('id', $segment_ids)->findMany();
$association->save(); foreach($segments as $segment) {
$association = SubscriberSegment::create();
$association->subscriber_id = $this->id;
$association->segment_id = $segment->id;
$association->save();
}
} }
} }
@ -53,7 +58,7 @@ class Subscriber extends Model {
return false; return false;
} }
$subscriber = static::createOrUpdate($subscriber_data); $subscriber = self::createOrUpdate($subscriber_data);
if($subscriber !== false && $subscriber->id() > 0) { if($subscriber !== false && $subscriber->id() > 0) {
// restore deleted subscriber // restore deleted subscriber
@ -92,20 +97,25 @@ class Subscriber extends Model {
$segments = Segment::orderByAsc('name')->findMany(); $segments = Segment::orderByAsc('name')->findMany();
$segment_list = array(); $segment_list = array();
$segment_list[] = array( $segment_list[] = array(
'label' => __('All lists'), 'label' => __('All segments'),
'value' => '' 'value' => ''
); );
$segment_list[] = array(
'label' => sprintf(
__('Subscribers without a segment (%d)'),
self::filter('withoutSegments')->count()
),
'value' => 'none'
);
foreach($segments as $segment) { foreach($segments as $segment) {
$subscribers_count = $segment->subscribers() $subscribers_count = $segment->subscribers()
->whereNull('deleted_at') ->whereNull('deleted_at')
->count(); ->count();
if($subscribers_count > 0) { $segment_list[] = array(
$segment_list[] = array( 'label' => sprintf('%s (%d)', $segment->name, $subscribers_count),
'label' => sprintf('%s (%d)', $segment->name, $subscribers_count), 'value' => $segment->id()
'value' => $segment->id() );
);
}
} }
$filters = array( $filters = array(
@ -121,9 +131,13 @@ class Subscriber extends Model {
} }
foreach($filters as $key => $value) { foreach($filters as $key => $value) {
if($key === 'segment') { if($key === 'segment') {
$segment = Segment::findOne($value); if($value === 'none') {
if($segment !== false) { return self::filter('withoutSegments');
return $segment->subscribers(); } else {
$segment = Segment::findOne($value);
if($segment !== false) {
return $segment->subscribers();
}
} }
} }
} }
@ -135,27 +149,27 @@ class Subscriber extends Model {
array( array(
'name' => 'all', 'name' => 'all',
'label' => __('All'), 'label' => __('All'),
'count' => Subscriber::getPublished()->count() 'count' => self::getPublished()->count()
), ),
array( array(
'name' => 'subscribed', 'name' => 'subscribed',
'label' => __('Subscribed'), 'label' => __('Subscribed'),
'count' => Subscriber::filter('subscribed')->count() 'count' => self::filter('subscribed')->count()
), ),
array( array(
'name' => 'unconfirmed', 'name' => 'unconfirmed',
'label' => __('Unconfirmed'), 'label' => __('Unconfirmed'),
'count' => Subscriber::filter('unconfirmed')->count() 'count' => self::filter('unconfirmed')->count()
), ),
array( array(
'name' => 'unsubscribed', 'name' => 'unsubscribed',
'label' => __('Unsubscribed'), 'label' => __('Unsubscribed'),
'count' => Subscriber::filter('unsubscribed')->count() 'count' => self::filter('unsubscribed')->count()
), ),
array( array(
'name' => 'trash', 'name' => 'trash',
'label' => __('Trash'), 'label' => __('Trash'),
'count' => Subscriber::getTrashed()->count() 'count' => self::getTrashed()->count()
) )
); );
} }
@ -226,28 +240,91 @@ class Subscriber extends Model {
$subscriber = false; $subscriber = false;
if(isset($data['id']) && (int)$data['id'] > 0) { if(isset($data['id']) && (int)$data['id'] > 0) {
$subscriber = static::findOne((int)$data['id']); $subscriber = self::findOne((int)$data['id']);
unset($data['id']); unset($data['id']);
} }
if($subscriber === false && !empty($data['email'])) { if($subscriber === false && !empty($data['email'])) {
$subscriber = static::where('email', $data['email'])->findOne(); $subscriber = self::where('email', $data['email'])->findOne();
if($subscriber !== false) { if($subscriber !== false) {
unset($data['email']); unset($data['email']);
} }
} }
// segments
$segment_ids = array();
if(isset($data['segments'])) {
$segment_ids = (array)$data['segments'];
unset($data['segments']);
}
// custom fields
$custom_fields = array();
foreach($data as $key => $value) {
if(strpos($key, 'cf_') === 0) {
$custom_fields[(int)substr($key, 3)] = $value;
unset($data[$key]);
}
}
if($subscriber === false) { if($subscriber === false) {
$subscriber = static::create(); $subscriber = self::create();
$subscriber->hydrate($data); $subscriber->hydrate($data);
} else { } else {
$subscriber->set($data); $subscriber->set($data);
} }
$subscriber->save(); if($subscriber->save()) {
if(!empty($custom_fields)) {
foreach($custom_fields as $custom_field_id => $value) {
$subscriber->setCustomField($custom_field_id, $value);
}
}
$subscriber->addToSegments($segment_ids);
}
return $subscriber; return $subscriber;
} }
function withCustomFields() {
$custom_fields = CustomField::select('id')->findArray();
if(empty($custom_fields)) return $this;
$custom_field_ids = Helpers::arrayColumn($custom_fields, 'id');
$relations = SubscriberCustomField::select('custom_field_id')
->select('value')
->whereIn('custom_field_id', $custom_field_ids)
->where('subscriber_id', $this->id())
->findMany();
foreach($relations as $relation) {
$this->{'cf_'.$relation->custom_field_id} = $relation->value;
}
return $this;
}
function getCustomField($custom_field_id, $default = null) {
$custom_field = SubscriberCustomField::select('value')
->where('custom_field_id', $custom_field_id)
->where('subscriber_id', $this->id())
->findOne();
if($custom_field === false) {
return $default;
} else {
return $custom_field->value;
}
}
function setCustomField($custom_field_id, $value) {
return SubscriberCustomField::createOrUpdate(array(
'subscriber_id' => $this->id(),
'custom_field_id' => $custom_field_id,
'value' => $value
));
}
static function bulkMoveToList($orm, $data = array()) { static function bulkMoveToList($orm, $data = array()) {
$segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0); $segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0);
$segment = Segment::findOne($segment_id); $segment = Segment::findOne($segment_id);
@ -376,6 +453,19 @@ class Subscriber extends Model {
->where('status', 'unconfirmed'); ->where('status', 'unconfirmed');
} }
static function withoutSegments($orm) {
return $orm->select(MP_SUBSCRIBERS_TABLE.'.*')
->leftOuterJoin(
MP_SUBSCRIBER_SEGMENT_TABLE,
array(
MP_SUBSCRIBERS_TABLE.'.id',
'=',
MP_SUBSCRIBER_SEGMENT_TABLE.'.subscriber_id'
)
)
->whereNull(MP_SUBSCRIBER_SEGMENT_TABLE.'.subscriber_id');
}
static function createMultiple($columns, $values) { static function createMultiple($columns, $values) {
return self::rawExecute( return self::rawExecute(
'INSERT INTO `' . self::$_table . '` ' . 'INSERT INTO `' . self::$_table . '` ' .

View File

@ -12,6 +12,49 @@ class SubscriberCustomField extends Model {
parent::__construct(); parent::__construct();
} }
static function createOrUpdate($data = array()) {
$custom_field = CustomField::findOne($data['custom_field_id']);
if($custom_field === false) {
return false;
} else {
$custom_field = $custom_field->asArray();
}
if($custom_field['type'] === 'date') {
if(is_array($data['value'])) {
$day = (
isset($data['value']['day'])
? (int)$data['value']['day']
: 1
);
$month = (
isset($data['value']['month'])
? (int)$data['value']['month']
: 1
);
$year = (
isset($data['value']['year'])
? (int)$data['value']['year']
: 1970
);
$data['value'] = mktime(0, 0, 0, $month, $day, $year);
}
}
$relation = self::where('custom_field_id', $data['custom_field_id'])
->where('subscriber_id', $data['subscriber_id'])
->findOne();
if($relation === false) {
$relation = self::create();
$relation->hydrate($data);
} else {
$relation->set($data);
}
return $relation->save();
}
static function createMultiple($values) { static function createMultiple($values) {
$values = array_map('array_values', $values); $values = array_map('array_values', $values);
return self::rawExecute( return self::rawExecute(

View File

@ -0,0 +1,69 @@
<?php
namespace MailPoet\Newsletter;
use MailPoet\Newsletter\Editor\Transformer;
if(!defined('ABSPATH')) exit;
class AutomatedLatestContent {
function getPosts($args) {
$parameters = array(
'posts_per_page' => (isset($args['amount'])) ? (int) $args['amount'] : 10,
'post_type' => (isset($args['contentType'])) ? $args['contentType'] : 'post',
'post_status' => (isset($args['postStatus'])) ? $args['postStatus'] : 'publish',
'orderby' => 'date',
'order' => ($args['sortBy'] === 'newest') ? 'DESC' : 'ASC',
);
if(isset($args['search'])) {
$parameters['s'] = $args['search'];
}
if(isset($args['posts']) && is_array($args['posts'])) {
$parameters['post__in'] = $args['posts'];
}
$parameters['tax_query'] = $this->constructTaxonomiesQuery($args);
return get_posts($parameters);
}
function transformPosts($args, $posts) {
$transformer = new Transformer($args);
return $transformer->transform($posts);
}
function constructTaxonomiesQuery($args) {
$taxonomies_query = array();
if(isset($args['terms']) && is_array($args['terms'])) {
// Add filtering by tags and categories
$tags = array();
$categories = array();
foreach($args['terms'] as $term) {
if($term['taxonomy'] === 'category') {
$categories[] = $term['id'];
} else if($term['taxonomy'] === 'post_tag') $tags[] = $term['id'];
}
$taxonomies = array(
'post_tag' => $tags,
'category' => $categories
);
foreach($taxonomies as $taxonomy => $terms) {
if(!empty($terms)) {
$tax = array(
'taxonomy' => $taxonomy,
'field' => 'id',
'terms' => $terms,
);
if($args['inclusionType'] === 'exclude') $tax['operator'] = 'NOT IN';
$taxonomies_query[] = $tax;
}
}
if(!empty($taxonomies_query)) {
// With exclusion we want to use 'AND', because we want posts that
// don't have excluded tags/categories. But with inclusion we want to
// use 'OR', because we want posts that have any of the included
// tags/categories
$taxonomies_query['relation'] = ($args['inclusionType'] === 'exclude') ? 'AND' : 'OR';
return $taxonomies_query;
}
}
return $taxonomies_query;
}
}

View File

@ -22,12 +22,12 @@ class PostTransformer {
$content = $content_manager->filterContent($content); $content = $content_manager->filterContent($content);
$structure_transformer = new StructureTransformer(); $structure_transformer = new StructureTransformer();
$structure = $structure_transformer->transform($content, $this->args['imagePadded'] === 'true'); $structure = $structure_transformer->transform($content, $this->args['imageFullWidth'] === 'true');
$structure = $this->appendFeaturedImage( $structure = $this->appendFeaturedImage(
$post, $post,
$this->args['displayType'], $this->args['displayType'],
$this->args['imagePadded'] === 'true', $this->args['imageFullWidth'] === 'true',
$structure $structure
); );
$structure = $this->appendPostTitle($post, $structure); $structure = $this->appendPostTitle($post, $structure);
@ -36,7 +36,7 @@ class PostTransformer {
return $structure; return $structure;
} }
private function appendFeaturedImage($post, $display_type, $image_padded, $structure) { private function appendFeaturedImage($post, $display_type, $image_full_width, $structure) {
if ($display_type === 'full') { if ($display_type === 'full') {
// No featured images for full posts // No featured images for full posts
return $structure; return $structure;
@ -45,7 +45,7 @@ class PostTransformer {
$featured_image = $this->getFeaturedImage( $featured_image = $this->getFeaturedImage(
$post->ID, $post->ID,
$post->post_title, $post->post_title,
(bool)$image_padded (bool)$image_full_width
); );
if (is_array($featured_image)) { if (is_array($featured_image)) {
@ -55,14 +55,14 @@ class PostTransformer {
return $structure; return $structure;
} }
private function getFeaturedImage($post_id, $post_title, $image_padded) { private function getFeaturedImage($post_id, $post_title, $image_full_width) {
if(has_post_thumbnail($post_id)) { if(has_post_thumbnail($post_id)) {
$thumbnail_id = get_post_thumbnail_id($post_id); $thumbnail_id = get_post_thumbnail_id($post_id);
// get attachment data (src, width, height) // get attachment data (src, width, height)
$image_info = wp_get_attachment_image_src( $image_info = wp_get_attachment_image_src(
$thumbnail_id, $thumbnail_id,
'single-post-thumbnail' 'mailpoet_newsletter_max'
); );
// get alt text // get alt text
@ -81,7 +81,7 @@ class PostTransformer {
'link' => get_permalink($post_id), 'link' => get_permalink($post_id),
'src' => $image_info[0], 'src' => $image_info[0],
'alt' => $alt_text, 'alt' => $alt_text,
'padded' => $image_padded, 'fullWidth' => $image_full_width,
'width' => $image_info[1], 'width' => $image_info[1],
'height' => $image_info[2], 'height' => $image_info[2],
'styles' => array( 'styles' => array(

View File

@ -7,11 +7,11 @@ if(!defined('ABSPATH')) exit;
class StructureTransformer { class StructureTransformer {
function transform($content, $image_padded) { function transform($content, $image_full_width) {
$root = pQuery::parseStr($content); $root = pQuery::parseStr($content);
$this->hoistImagesToRoot($root); $this->hoistImagesToRoot($root);
$structure = $this->transformTagsToBlocks($root, $image_padded); $structure = $this->transformTagsToBlocks($root, $image_full_width);
$structure = $this->mergeNeighboringBlocks($structure); $structure = $this->mergeNeighboringBlocks($structure);
return $structure; return $structure;
} }
@ -44,8 +44,8 @@ class StructureTransformer {
* Transforms HTML tags into their respective JSON objects, * Transforms HTML tags into their respective JSON objects,
* turns other root children into text blocks * turns other root children into text blocks
*/ */
private function transformTagsToBlocks($root, $image_padded) { private function transformTagsToBlocks($root, $image_full_width) {
return array_map(function($item) use ($image_padded) { return array_map(function($item) use ($image_full_width) {
if ($item->tag === 'img' || $item->tag === 'a' && $item->query('img')) { if ($item->tag === 'img' || $item->tag === 'a' && $item->query('img')) {
if ($item->tag === 'a') { if ($item->tag === 'a') {
$link = $item->getAttribute('href'); $link = $item->getAttribute('href');
@ -60,7 +60,7 @@ class StructureTransformer {
'link' => $link, 'link' => $link,
'src' => $image->getAttribute('src'), 'src' => $image->getAttribute('src'),
'alt' => $image->getAttribute('alt'), 'alt' => $image->getAttribute('alt'),
'padded' => $image_padded, 'fullWidth' => $image_full_width,
'width' => $image->getAttribute('width'), 'width' => $image->getAttribute('width'),
'height' => $image->getAttribute('height'), 'height' => $image->getAttribute('height'),
'styles' => array( 'styles' => array(

View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Newsletter\Renderer\Blocks;
class AutomatedLatestContent {
static function render($element, $column_count) {
$ALC = new \MailPoet\Newsletter\AutomatedLatestContent();
$posts = $ALC->getPosts($element);
$transformed_posts = array('blocks' => $ALC->transformPosts($element, $posts));
$renderer = new Renderer();
return $renderer->render($transformed_posts, $column_count);
}
}

View File

@ -5,21 +5,23 @@ use MailPoet\Newsletter\Renderer\StylesHelper;
class Footer { class Footer {
static function render($element) { static function render($element) {
if(isset($element['styles']['link'])) {
$element['text'] = str_replace(
'<a',
'<a style="'
. StylesHelper::getStyles($element['styles'], 'link')
. '"', $element['text']
);
}
$element['text'] = preg_replace('/\n/', '<br /><br />', $element['text']); $element['text'] = preg_replace('/\n/', '<br /><br />', $element['text']);
$element['text'] = preg_replace('/(<\/?p>)/', '', $element['text']); $element['text'] = preg_replace('/(<\/?p.*?>)/i', '', $element['text']);
$DOM_parser = new \pQuery();
$DOM = $DOM_parser->parseStr($element['text']);
if(isset($element['styles']['link'])) {
$links = $DOM->query('a');
if($links->count()) {
foreach($links as $link) {
$link->style = StylesHelper::getStyles($element['styles'], 'link');
}
}
}
$template = ' $template = '
<tr> <tr>
<td class="mailpoet_padded_header_footer mailpoet_footer" bgcolor="' . $element['styles']['block']['backgroundColor'] . '" <td class="mailpoet_padded_header_footer mailpoet_footer" bgcolor="' . $element['styles']['block']['backgroundColor'] . '"
style="' . StylesHelper::getBlockStyles($element) . StylesHelper::getStyles($element['styles'], 'text') . '"> style="' . StylesHelper::getBlockStyles($element) . StylesHelper::getStyles($element['styles'], 'text') . '">
' . $element['text'] . ' ' . $DOM->html() . '
</td> </td>
</tr>'; </tr>';
return $template; return $template;

View File

@ -5,21 +5,23 @@ use MailPoet\Newsletter\Renderer\StylesHelper;
class Header { class Header {
static function render($element) { static function render($element) {
if(isset($element['styles']['link'])) {
$element['text'] = str_replace(
'<a',
'<a style="'
. StylesHelper::getStyles($element['styles'], 'link')
. '"', $element['text']
);
}
$element['text'] = preg_replace('/\n/', '<br /><br />', $element['text']); $element['text'] = preg_replace('/\n/', '<br /><br />', $element['text']);
$element['text'] = preg_replace('/(<\/?p>)/', '', $element['text']); $element['text'] = preg_replace('/(<\/?p.*?>)/', '', $element['text']);
$DOM_parser = new \pQuery();
$DOM = $DOM_parser->parseStr($element['text']);
if(isset($element['styles']['link'])) {
$links = $DOM->query('a');
if($links->count()) {
foreach($links as $link) {
$link->style = StylesHelper::getStyles($element['styles'], 'link');
}
}
}
$template = ' $template = '
<tr> <tr>
<td class="mailpoet_padded_header_footer mailpoet_header" bgcolor="' . $element['styles']['block']['backgroundColor'] . '" <td class="mailpoet_padded_header_footer mailpoet_header" bgcolor="' . $element['styles']['block']['backgroundColor'] . '"
style="' . StylesHelper::getBlockStyles($element) . StylesHelper::getStyles($element['styles'], 'text') . '"> style="' . StylesHelper::getBlockStyles($element) . StylesHelper::getStyles($element['styles'], 'text') . '">
' . $element['text'] . ' ' . $DOM->html() . '
</td> </td>
</tr>'; </tr>';
return $template; return $template;

View File

@ -6,10 +6,12 @@ use MailPoet\Newsletter\Renderer\StylesHelper;
class Image { class Image {
static function render($element, $columnCount) { static function render($element, $columnCount) {
$element = self::getImageDimensions($element, $columnCount); $element['width'] = (int) $element['width'];
$element['height'] = (int) $element['height'];
$element = self::adjustImageDimensions($element, $columnCount);
$template = ' $template = '
<tr> <tr>
<td class="mailpoet_image ' . $element['paddedClass'] . '" align="center" valign="top"> <td class="mailpoet_image ' . (($element['fullWidth'] === false) ? 'mailpoet_padded' : '') . '" align="center" valign="top">
<img style="max-width:' . $element['width'] . 'px;" src="' . $element['src'] . '" <img style="max-width:' . $element['width'] . 'px;" src="' . $element['src'] . '"
width="' . $element['width'] . '" height="' . $element['height'] . '" alt="' . $element['alt'] . '"/> width="' . $element['width'] . '" height="' . $element['height'] . '" alt="' . $element['alt'] . '"/>
</td> </td>
@ -17,25 +19,22 @@ class Image {
return $template; return $template;
} }
static function getImageDimensions($element, $column_count) { static function adjustImageDimensions($element, $column_count) {
$column_width = ColumnsHelper::columnWidth($column_count); $column_width = ColumnsHelper::columnWidth($column_count);
$padded_width = StylesHelper::$padding_width * 2; $padded_width = StylesHelper::$padding_width * 2;
// resize image if it's wider than the column width // scale image to fit column width
if((int) $element['width'] >= $column_width) { if($element['width'] > $column_width) {
$ratio = (int) $element['width'] / $column_width; $ratio = $element['width'] / $column_width;
$element['width'] = $column_width; $element['width'] = $column_width;
$element['height'] = ceil((int) $element['height'] / $ratio); $element['height'] = (int) ceil($element['height'] / $ratio);
} }
if($element['padded'] == "true" && $element['width'] >= $column_width) { // resize image if the image is padded and wider than padded column width
// resize image if the padded option is on if($element['fullWidth'] === false &&
$ratio = (int) $element['width'] / ((int) $element['width'] - $padded_width); $element['width'] > ($column_width - $padded_width)
$element['width'] = (int) $element['width'] - $padded_width; ) {
$element['height'] = ceil((int) $element['height'] / $ratio); $ratio = $element['width'] / ($column_width - $padded_width);
$element['paddedClass'] = 'mailpoet_padded'; $element['width'] = $column_width - $padded_width;
} else { $element['height'] = (int) ceil($element['height'] / $ratio);
$element['width'] = (int) $element['width'];
$element['height'] = (int) $element['height'];
$element['paddedClass'] = '';
} }
return $element; return $element;
} }

View File

@ -6,10 +6,9 @@ class Text {
$html = $element['text']; $html = $element['text'];
$html = self::convertBlockquotesToTables($html); $html = self::convertBlockquotesToTables($html);
$html = self::convertParagraphsToTables($html); $html = self::convertParagraphsToTables($html);
$html = self::addLineBreakAfterTags($html);
$html = self::styleLists($html); $html = self::styleLists($html);
$html = self::styleHeadings($html); $html = self::styleHeadings($html);
$html = self::removeLastElementBreakLine($html); $html = self::addLineBreakAfterTags($html);
$template = ' $template = '
<tr> <tr>
<td class="mailpoet_text mailpoet_padded" valign="top" style="word-break:break-word;word-wrap:break-word;"> <td class="mailpoet_text mailpoet_padded" valign="top" style="word-break:break-word;word-wrap:break-word;">
@ -19,42 +18,26 @@ class Text {
return $template; return $template;
} }
static function convertParagraphsToTables($html) {
$html = preg_replace('/<p>(.*?)<\/p>/', '
<table width="100%" cellpadding="0" cellspacing="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="mailpoet_paragraph" style="word-break:break-word;word-wrap:break-word;">
$1
<br /><br />
</td>
</tr>
</table>'
, $html);
$html = preg_replace('/<p style=\"(.*)\">(.*?)<\/p>/', '
<table width="100%" cellpadding="0" cellspacing="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="mailpoet_paragraph" style="word-break:break-word;word-wrap:break-word;$1">
$2
<br /><br />
</td>
</tr>
</table>'
, $html);
return $html;
}
static function removeLastElementBreakLine($html) {
return preg_replace('/<br\/>([^<br\/>]*)$/s', '', $html);
}
static function addLineBreakAfterTags($html) {
return preg_replace('/(<\/(ul|ol|h\d)>)/', '$1<br />', $html);
}
static function convertBlockquotesToTables($html) { static function convertBlockquotesToTables($html) {
$template = ' $DOM_parser = new \pQuery();
<table width="100%" cellpadding="0" cellspacing="0" border="0"> $DOM = $DOM_parser->parseStr($html);
$blockquotes = $DOM->query('blockquote');
if(!$blockquotes->count()) return $html;
foreach($blockquotes as $blockquote) {
$paragraphs = $blockquote->query('p', 0);
foreach($paragraphs as $index => $paragraph) {
$contents[] = $paragraph->html();
if($index + 1 < $paragraphs->count()) $contents[] = '<br />';
$paragraph->remove();
}
$paragraph->remove();
$blockquote->setTag('table');
$blockquote->addClass('mailpoet_blockquote');
$blockquote->width = '100%';
$blockquote->spacing = 0;
$blockquote->border = 0;
$blockquote->cellpadding = 0;
$blockquote->html('
<tbody> <tbody>
<tr> <tr>
<td width="2" bgcolor="#565656"></td> <td width="2" bgcolor="#565656"></td>
@ -63,48 +46,90 @@ class Text {
<table style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0"> <table style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
<tr> <tr>
<td class="mailpoet_blockquote"> <td class="mailpoet_blockquote">
$1 ' . implode('', $contents) . '
</td> </td>
</tr> </tr>
</table> </table>
</td> </td>
</tr> </tr>
</tbody> </tbody>'
</table>
<br/>';
preg_match('/<blockquote>.*?<\/blockquote>/s', $html, $blockquotes);
foreach($blockquotes as $index => $blockquote) {
$blockquote = preg_replace('/<\/p>\n<p>/', '<br/><br/>', $blockquote);
$blockquote = preg_replace('/<\/?p>/', '', $blockquote);
$blockquote = preg_replace(
'/<blockquote>(.*?)<\/blockquote>/s',
$template,
$blockquote
);
$html = preg_replace(
'/' . preg_quote($blockquotes[$index], '/') . '/',
$blockquote,
$html
); );
} }
return $html; return $DOM->__toString();
} }
static function styleHeadings($html) { static function convertParagraphsToTables($html) {
return preg_replace( $DOM_parser = new \pQuery();
'/<(h[1-6])(?:.+style=\"(.*)?\")?>/', $DOM = $DOM_parser->parseStr($html);
'<$1 style="margin:0;font-style:normal;font-weight:normal;$2">', $paragraphs = $DOM->query('p');
$html if(!$paragraphs->count()) return $html;
); foreach($paragraphs as $paragraph) {
// remove empty paragraphs
if(!trim($paragraph->html())) {
$paragraph->remove();
continue;
}
$style = $paragraph->style;
$contents = $paragraph->html();
$paragraph->setTag('table');
$paragraph->style = 'border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;';
$paragraph->width = '100%';
$paragraph->cellpadding = 0;
$paragraph->html('
<tr>
<td class="mailpoet_paragraph" style="word-break:break-word;word-wrap:break-word;' . $style . '">
' . $contents . '
<br /><br />
</td>
</tr>'
);
}
return $DOM->__toString();
} }
static function styleLists($html) { static function styleLists($html) {
$html = preg_replace( $DOM_parser = new \pQuery();
'/<(ul|ol)>/', $DOM = $DOM_parser->parseStr($html);
'<$1 class="mailpoet_paragraph" style="padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;">', $lists = $DOM->query('ol, ul, li');
$html if(!$lists->count()) return $html;
); foreach($lists as $list) {
$html = preg_replace('/<li>/', '<li class="mailpoet_paragraph">', $html); if($list->tag === 'li') {
return $html; $list->class = 'mailpoet_paragraph';
} else {
$list->class = 'mailpoet_paragraph';
$list->style .= 'padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;';
}
}
return $DOM->__toString();
}
static function styleHeadings($html) {
$DOM_parser = new \pQuery();
$DOM = $DOM_parser->parseStr($html);
$headings = $DOM->query('h1, h2, h3, h4');
if(!$headings->count()) return $html;
foreach($headings as $heading) {
$heading->style .= 'margin:0;font-style:normal;font-weight:normal;';
}
return $DOM->__toString();
}
static function addLineBreakAfterTags($html) {
$DOM_parser = new \pQuery();
$DOM = $DOM_parser->parseStr($html);
$tags = $DOM->query('ul, ol, h1, h2, h3, h4, table.mailpoet_blockquote');
if(!$tags->count()) return $html;
foreach($tags as $tag) {
$tag->parent->insertChild(
array(
'tag_name' => 'br',
'self_close' => true,
'attributes' => array()
),
$tag->index() + 1
);
}
// remove last line break
return preg_replace('/(^)?(<br.*?\/?>)+$/i', '', $DOM->__toString());
} }
} }

View File

@ -35,7 +35,11 @@ class Renderer {
$newsletter_body $newsletter_body
)); ));
$template = $this->inlineCSSStyles($template); $template = $this->inlineCSSStyles($template);
return $this->postProcessTemplate($template); $template = $this->postProcessTemplate($template);
return array(
'html' => $template,
'text' => $this->renderTextVersion($template)
);
} }
function renderBody($content) { function renderBody($content) {
@ -65,7 +69,7 @@ class Renderer {
$selector = 'h3'; $selector = 'h3';
break; break;
case 'text': case 'text':
$selector = '.mailpoet_paragraph, .mailpoet_blockquote'; $selector = '.mailpoet_paragraph, td.mailpoet_blockquote';
break; break;
case 'body': case 'body':
$selector = 'body, .mailpoet-wrapper'; $selector = 'body, .mailpoet-wrapper';
@ -107,8 +111,8 @@ class Renderer {
} }
function renderTextVersion($template) { function renderTextVersion($template) {
// TODO: add text rendering $template = mb_convert_encoding($template, 'HTML-ENTITIES', 'UTF-8');
return $template; return \Html2Text\Html2Text::convert($template);
} }
function postProcessTemplate($template) { function postProcessTemplate($template) {
@ -118,7 +122,6 @@ class Renderer {
$template->html( $template->html(
str_replace('!important', '', $template->html()) str_replace('!important', '', $template->html())
); );
// TODO: return array with html and text body
return $DOM->__toString(); return $DOM->__toString();
} }
} }

View File

@ -0,0 +1,43 @@
<?php
namespace MailPoet\Newsletter\Shortcodes\Categories;
class Date {
/*
{
text: '<%= __('Current day of the month number') %>',
shortcode: 'date:d',
},
{
text: '<%= __('Current day of the month in ordinal, ie. 2nd, 3rd, etc.') %>',
shortcode: 'date:dordinal',
},
{
text: '<%= __('Full name of current day') %>',
shortcode: 'date:dtext',
},
{
text: '<%= __('Current month number') %>',
shortcode: 'date:m',
},
{
text: '<%= __('Full name of current month') %>',
shortcode: 'date:mtext',
},
{
text: '<%= __('Year') %>',
shortcode: 'date:y',
}
*/
static function process($action) {
$date = new \DateTime('now');
$actions = array(
'd' => $date->format('d'),
'dordinal' => $date->format('dS'),
'dtext' => $date->format('D'),
'm' => $date->format('m'),
'mtext' => $date->format('F'),
'y' => $date->format('Y')
);
return (isset($actions[$action])) ? $actions[$action] : false;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace MailPoet\Newsletter\Shortcodes\Categories;
require_once(ABSPATH . 'wp-includes/pluggable.php');
class Link {
/*
{
text: '<%= __('Unsubscribe link') %>',
shortcode: 'global:unsubscribe',
},
{
text: '<%= __('Edit subscription page link') %>',
shortcode: 'global:manage',
},
{
text: '<%= __('View in browser link') %>',
shortcode: 'global:browser',
}
*/
static function process($action) {
// TODO: implement
$actions = array(
'unsubscribe' => '',
'manage' => '',
'browser' => ''
);
return (isset($actions[$action])) ? $actions[$action] : false;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace MailPoet\Newsletter\Shortcodes\Categories;
class Newsletter {
/*
{
text: '<%= __('Newsletter Subject') %>',-
shortcode: 'newsletter:subject',
},
{
text: '<%= __('Total number of posts or pages') %>',
shortcode: 'newsletter:total',
},
{
text: '<%= __('Latest post title') %>',
shortcode: 'newsletter:post_title',
},
{
text: '<%= __('Issue number') %>',
shortcode: 'newsletter:number',
}
*/
static function process($action, $default_value = false, $newsletter) {
if(is_object($newsletter)) {
$newsletter = $newsletter->asArray();
}
switch($action) {
case 'subject':
return ($newsletter) ? $newsletter['subject'] : false;
case 'total':
$posts = wp_count_posts();
return $posts->publish;
case 'post_title':
$post = wp_get_recent_posts(array('numberposts' => 1));
return (isset($post[0])) ? $post[0]['post_title'] : false;
case 'number':
// TODO: implement
return;
default:
return false;
}
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace MailPoet\Newsletter\Shortcodes\Categories;
use MailPoet\Models\Subscriber;
require_once(ABSPATH . 'wp-includes/pluggable.php');
class User {
/*
{
text: '<%= __('First Name') %>',
shortcode: 'user:firstname | default:reader',
},
{
text: '<%= __('Last Name') %>',
shortcode: 'user:lastname | default:reader',
},
{
text: '<%= __('Email Address') %>',
shortcode: 'user:email',
},
{
text: '<%= __('Wordpress user display name') %>',
shortcode: 'user:displayname | default:member',
},
{
text: '<%= __('Total of subscribers') %>',
shortcode: 'user:count',
}
*/
static function process($action, $default_value, $newsletter = false, $subscriber) {
if(is_object($subscriber)) {
$subscriber = $subscriber->asArray();
}
switch($action) {
case 'firstname':
return ($subscriber) ? $subscriber['first_name'] : $default_value;
case 'lastname':
return ($subscriber) ? $subscriber['last_name'] : $default_value;
case 'email':
return ($subscriber) ? $subscriber['email'] : false;
case 'displayname':
if($subscriber && $subscriber['wp_user_id']) {
$wp_user = get_userdata($subscriber['wp_user_id']);
return $wp_user->user_login;
};
return $default_value;
case 'count':
return Subscriber::count();
default:
return false;
}
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace MailPoet\Newsletter\Shortcodes;
class Shortcodes {
public $rendered_newsletter;
public $newsletter;
public $subscriber;
function __construct(
$rendered_newsletter,
$newsletter = false,
$subscriber = false) {
$this->rendered_newsletter = $rendered_newsletter;
$this->newsletter = $newsletter;
$this->subscriber = $subscriber;
}
function extract() {
preg_match_all('/\[(?:\w+):.*?\]/', $this->rendered_newsletter, $shortcodes);
return array_unique($shortcodes[0]);
}
function process($shortcodes) {
$processed_shortcodes = array_map(
function ($shortcode) {
// TODO: discuss renaming "global". It is a reserved name in PHP.
if($shortcode === 'global') $shortcode = 'link';
preg_match(
'/\[(?P<type>\w+):(?P<action>\w+)(?:.*?default:(?P<default>.*?))?\]/',
$shortcode,
$shortcode_details
);
$shortcode_class =
__NAMESPACE__ . '\\Categories\\' . ucfirst($shortcode_details['type']);
if(!class_exists($shortcode_class)) return false;
return $shortcode_class::process(
$shortcode_details['action'],
isset($shortcode_details['default'])
? $shortcode_details['default'] : false,
$this->newsletter,
$this->subscriber
);
}, $shortcodes);
return array_filter($processed_shortcodes);
}
function replace() {
$shortcodes = $this->extract($this->rendered_newsletter);
$processed_shortcodes = $this->process($shortcodes);
$shortcodes = array_intersect_key($shortcodes, $processed_shortcodes);
return str_replace($shortcodes, $processed_shortcodes, $this->rendered_newsletter);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace MailPoet\Router;
if(!defined('ABSPATH')) exit;
class AutomatedLatestContent {
public $ALC;
function __construct() {
$this->ALC = new \MailPoet\Newsletter\AutomatedLatestContent();
}
function getPostTypes() {
return get_post_types(array(), 'objects');
}
function getTaxonomies($args) {
$post_type = (isset($args['postType'])) ? $args['postType'] : 'post';
return get_object_taxonomies($post_type, 'objects');
}
function getTerms($args) {
$taxonomies = (isset($args['taxonomies'])) ? $args['taxonomies'] : array();
$search = (isset($args['search'])) ? $args['search'] : '';
$limit = (isset($args['limit'])) ? (int)$args['limit'] : 10;
$page = (isset($args['page'])) ? (int)$args['page'] : 1;
return get_terms(
$taxonomies,
array(
'hide_empty' => false,
'search' => $search,
'number' => $limit,
'offset' => $limit * ($page - 1)
)
);
}
function getPosts($args) {
return $this->ALC->getPosts($args);
}
function getTransformedPosts($args) {
$posts = $this->ALC->getPosts($args);
return $this->ALC->transformPosts($args, $posts);
}
}

Some files were not shown because too many files have changed in this diff Show More