Compare commits

...

130 Commits

Author SHA1 Message Date
6088497433 Bump up release version to 0.0.15 2016-02-12 18:25:12 +02:00
57f0b88299 Merge pull request #347 from mailpoet/sending_frequency
Implements sending frequency
2016-02-12 14:23:22 +02: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
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
178 changed files with 5176 additions and 3253 deletions

View File

@ -1,2 +1,16 @@
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) {
$this->loadEnv();
$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() {

View File

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

View File

@ -1,31 +1,31 @@
define([
'react',
'react-checkbox-group'
'react'
],
function(
React,
CheckboxGroup
React
) {
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() {
var selected_values = 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;
const isChecked = !!(this.props.item[this.props.field.name]);
var options = Object.keys(this.props.field.values).map(
const options = Object.keys(this.props.field.values).map(
function(value, index) {
return (
<p key={ 'checkbox-' + index }>
<label>
<input type="checkbox" value={ value } />
&nbsp;{ this.props.field.values[value] }
<input
ref="checkbox"
type="checkbox"
value="1"
checked={ isChecked }
onChange={ this.onValueChange }
name={ this.props.field.name }
/>
{ this.props.field.values[value] }
</label>
</p>
);
@ -33,30 +33,10 @@ function(
);
return (
<CheckboxGroup
name={ this.props.field.name }
value={ selected_values }
ref={ this.props.field.name }
onChange={ this.handleValueChange }>
<div>
{ 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/radio.jsx',
'form/fields/checkbox.jsx',
'form/fields/selection.jsx'
'form/fields/selection.jsx',
'form/fields/date.jsx',
],
function(
React,
@ -14,7 +15,8 @@ function(
FormFieldSelect,
FormFieldRadio,
FormFieldCheckbox,
FormFieldSelection
FormFieldSelection,
FormFieldDate
) {
var FormField = React.createClass({
renderField: function(data, inline = false) {
@ -55,6 +57,10 @@ function(
case 'selection':
field = (<FormFieldSelection {...data} />);
break;
case 'date':
field = (<FormFieldDate {...data} />);
break;
}
if(inline === true) {
@ -66,10 +72,10 @@ function(
);
} else {
return (
<p key={ 'field-' + (data.index || 0) }>
<div key={ 'field-' + (data.index || 0) }>
{ field }
{ description }
</p>
</div>
);
}
},

View File

@ -7,7 +7,6 @@ function(
var FormFieldRadio = React.createClass({
render: function() {
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(
function(value, index) {
@ -20,7 +19,7 @@ function(
value={ value }
onChange={ this.props.onValueChange }
name={ this.props.field.name } />
&nbsp;{ this.props.field.values[value] }
{ this.props.field.values[value] }
</label>
</p>
);

View File

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

View File

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

View File

@ -617,6 +617,28 @@ var WysijaForm = {
// this is a url, so do not encode the protocol
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(), {
duration: 0.2,
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
if(effect.element.previous('.block_placeholder') !== undefined) {
effect.element.previous('.block_placeholder').remove();

View File

@ -25,58 +25,49 @@ const columns = [
const messages = {
onTrash: function(response) {
if(response) {
let 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);
}
var count = ~~response;
var message = null;
if(message !== null) {
MailPoet.Notice.success(message);
}
if(count === 1) {
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) {
if(response) {
let message = null;
if(~~response === 1) {
message = (
'1 form was permanently deleted.'
);
} else if(~~response > 1) {
message = (
'%$1d forms were permanently deleted.'
).replace('%$1d', ~~response);
}
var count = ~~response;
var message = null;
if(message !== null) {
MailPoet.Notice.success(message);
}
if(count === 1) {
message = (
'1 form was permanently deleted.'
);
} else {
message = (
'%$1d forms were permanently deleted.'
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onRestore: function(response) {
if(response) {
let 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);
}
var count = ~~response;
var message = null;
if(message !== null) {
MailPoet.Notice.success(message);
}
if(count === 1) {
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',
action: 'create'
}).done(function(response) {
if(response !== false) {
window.location = response;
if(response.result && response.form_id) {
window.location = mailpoet_form_edit_url + response.form_id;
}
});
},

View File

@ -14,6 +14,9 @@ function(
})
return this.props.onSelectFilter(filters);
},
handleEmptyTrash: function() {
return this.props.onEmptyTrash();
},
getAvailableFilters: function() {
let filters = this.props.filters;
@ -34,7 +37,7 @@ function(
const available_filters = this.getAvailableFilters()
.map(function(filter, i) {
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]
} else {
jQuery(`select[name="${filter}"]`).val('');
@ -60,9 +63,10 @@ function(
let button = false;
if(available_filters.length > 0) {
if (available_filters.length > 0) {
button = (
<input
id="post-query-submit"
onClick={ this.handleFilterAction }
type="submit"
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 (
<div className="alignleft actions actions">
{ available_filters }
{ button }
{ empty_trash }
</div>
);
}

View File

@ -504,10 +504,21 @@ define(
this.getItems();
}.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) {
if(
this.state.selection === false
&& this.state.selected_ids.length === 0
&& selected_ids !== 'all'
) {
return;
}
@ -520,8 +531,10 @@ define(
limit: 0,
filter: this.state.filter,
group: this.state.group,
search: this.state.search,
selection: selected_ids
search: this.state.search
}
if(selected_ids !== 'all') {
data.listing.selection = selected_ids;
}
MailPoet.Ajax.post({
@ -715,7 +728,10 @@ define(
<ListingFilters
filters={ this.state.filters }
filter={ this.state.filter }
onSelectFilter={ this.handleFilter } />
group={ this.state.group }
onSelectFilter={ this.handleFilter }
onEmptyTrash={ this.handleEmptyTrash }
/>
<ListingPages
count={ this.state.count }
page={ this.state.page }

View File

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

View File

@ -35,7 +35,7 @@ define([
titlePosition: 'inTextBlock', // 'inTextBlock'|'aboveBlock',
titleAlignment: 'left', // 'left'|'center'|'right'
titleIsLink: false, // false|true
imagePadded: true, // true|false
imageFullWidth: false, // true|false
//imageAlignment: 'centerPadded', // 'centerFull'|'centerPadded'|'left'|'right'|'alternate'|'none'
showAuthor: 'no', // 'no'|'aboveText'|'belowText'
authorPrecededBy: 'Author:',
@ -63,7 +63,7 @@ define([
initialize: function() {
base.BlockView.prototype.initialize.apply(this, arguments);
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('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_title_position": _.partial(this.changeField, "titlePosition"),
"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"),
"keyup .mailpoet_automated_latest_content_author_preceded_by": _.partial(this.changeField, "authorPrecededBy"),
"change .mailpoet_automated_latest_content_show_categories": _.partial(this.changeField, "showCategories"),

View File

@ -20,7 +20,7 @@ define([
link: 'http://example.org',
src: 'no-image.png',
alt: 'An image of...',
padded: true, // true | false - Padded or full width
fullWidth: true, // true | false
width: '64px',
height: '64px',
styles: {
@ -45,10 +45,10 @@ define([
this.toolsView = new Module.ImageBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
if (this.model.get('padded')) {
this.$el.removeClass('mailpoet_full_image');
} else {
if (this.model.get('fullWidth')) {
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_address": _.partial(this.changeField, "src"),
"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"),
"click .mailpoet_field_image_select_another_image": "showMediaManager",
"click .mailpoet_done_editing": "close",

View File

@ -46,7 +46,7 @@ define([
titlePosition: 'inTextBlock', // 'inTextBlock'|'aboveBlock',
titleAlignment: 'left', // 'left'|'center'|'right'
titleIsLink: false, // false|true
imagePadded: true, // true|false
imageFullWidth: false, // true|false
//imageAlignment: 'centerPadded', // 'centerFull'|'centerPadded'|'left'|'right'|'alternate'|'none'
showAuthor: 'no', // 'no'|'aboveText'|'belowText'
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.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('divider'), 'change', refreshTransformedPosts);
@ -396,7 +396,7 @@ define([
"change .mailpoet_posts_include_or_exclude": _.partial(this.changeField, "inclusionType"),
"change .mailpoet_posts_title_position": _.partial(this.changeField, "titlePosition"),
"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"),
"keyup .mailpoet_posts_author_preceded_by": _.partial(this.changeField, "authorPrecededBy"),
"change .mailpoet_posts_show_categories": _.partial(this.changeField, "showCategories"),

View File

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

View File

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

View File

@ -225,6 +225,11 @@ define([
showPreview: function() {
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({
endpoint: 'newsletters',
action: 'render',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -192,21 +192,28 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
error: function(message, options) {
this.show(jQuery.extend({}, {
type: 'error',
message: '<p>'+message+'</p>'
message: '<p>'+this.formatMessage(message)+'</p>'
}, options));
},
success: function(message, options) {
this.show(jQuery.extend({}, {
type: 'success',
message: '<p>'+message+'</p>'
message: '<p>'+this.formatMessage(message)+'</p>'
}, options));
},
system: function(message, options) {
this.show(jQuery.extend({}, {
type: 'system',
static: true,
message: message
message: '<p>'+this.formatMessage(message)+'</p>'
}, 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() {
var form = $(this);
form.parsley({
errorsWrapper: '<p></p>',
errorTemplate: '<span></span>'
}).on('form:submit', function(parsley) {
form.parsley().on('form:submit', function(parsley) {
var data = form.serializeObject() || {};

View File

@ -42,58 +42,49 @@ var columns = [
const messages = {
onTrash: function(response) {
if(response) {
let 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);
}
var count = ~~response;
var message = null;
if(message !== null) {
MailPoet.Notice.success(message);
}
if(count === 1) {
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) {
if(response) {
let message = null;
if(~~response === 1) {
message = (
'1 segment was permanently deleted.'
);
} else if(~~response > 1) {
message = (
'%$1d segments were permanently deleted.'
).replace('%$1d', ~~response);
}
var count = ~~response;
var message = null;
if(message !== null) {
MailPoet.Notice.success(message);
}
if(count === 1) {
message = (
'1 segment was permanently deleted.'
);
} else {
message = (
'%$1d segments were permanently deleted.'
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onRestore: function(response) {
if(response) {
let 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);
}
var count = ~~response;
var message = null;
if(message !== null) {
MailPoet.Notice.success(message);
}
if(count === 1) {
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);
}
};

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 = {
onUpdate: function() {
MailPoet.Notice.success('Subscriber successfully updated!');

View File

@ -8,12 +8,14 @@ define(
'papaparse',
'select2'
],
function (Backbone,
_,
jQuery,
MailPoet,
Handlebars,
Papa) {
function (
Backbone,
_,
jQuery,
MailPoet,
Handlebars,
Papa
) {
if (!jQuery('#mailpoet_subscribers_import').length) {
return;
}
@ -138,6 +140,14 @@ define(
*/
uploadElement.change(function () {
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(
uploadProcessButtonElement,
(this.value.trim() !== '') ? 'on' : 'off'
@ -183,7 +193,7 @@ define(
}).done(function (response) {
if (response.result === false) {
MailPoet.Notice.hide();
MailPoet.Notice.error(response.error, {
MailPoet.Notice.error(response.errors, {
timeout: 3000,
});
jQuery('.mailpoet_mailchimp-key-status')
@ -235,7 +245,7 @@ define(
}
else {
MailPoet.Notice.hide();
MailPoet.Notice(response.message, {
MailPoet.Notice.error(response.errors, {
timeout: 3000,
});
}
@ -416,7 +426,10 @@ define(
}
else {
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,
});
}
@ -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
if (subscribers.subscribersCount > (maxRowsToShow + 1)) {
subscribers.subscribers.splice(
@ -1069,7 +1082,7 @@ define(
}).done(function (response) {
MailPoet.Modal.loading(false);
if (response.result === false) {
MailPoet.Notice.error(response.error, {
MailPoet.Notice.error(response.errors, {
timeout: 3000,
});
} else {

View File

@ -19,3 +19,12 @@ modules:
user: ''
password: ''
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",
"phpseclib/phpseclib": "*",
"mtdowling/cron-expression": "^1.0",
"nesbot/carbon": "^1.21"
"nesbot/carbon": "^1.21",
"soundasleep/html2text": "^0.3.0"
},
"require-dev": {
"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 init() {
register_activation_hook(
Env::$file,
array($this, 'activate')
);
}
function activate() {
$migrator = new Migrator();
$migrator->up();

View File

@ -36,8 +36,9 @@ class Env {
self::$views_path = self::$path . '/views';
self::$assets_path = self::$path . '/assets';
self::$assets_url = plugins_url('/assets', $file);
self::$temp_path = wp_upload_dir()['path'];
self::$temp_URL = wp_upload_dir()['url'];
$wp_upload_dir = wp_upload_dir();
self::$temp_path = $wp_upload_dir['path'];
self::$temp_URL = $wp_upload_dir['url'];
self::$languages_path = self::$path . '/lang';
self::$lib_path = self::$path . '/lib';
self::$plugin_prefix = 'mailpoet_';
@ -72,19 +73,4 @@ class Env {
);
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() {
// Subscribe in comments
if((bool)Setting::getValue('subscribe.on_comment.enabled')) {
add_action(
'comment_form_after_fields',
'\MailPoet\Subscription\Comment::extendForm'
);
if(is_user_logged_in()) {
add_action(
'comment_form_field_comment',
'\MailPoet\Subscription\Comment::extendLoggedInForm'
);
} else {
add_action(
'comment_form_after_fields',
'\MailPoet\Subscription\Comment::extendLoggedOutForm'
);
}
add_action(
'comment_post',

View File

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

View File

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

View File

@ -296,7 +296,7 @@ class Menu {
} else {
// check if users can register
$flags['registration_enabled'] =
(bool) get_option('users_can_register', false);
(bool)get_option('users_can_register', false);
}
return $flags;
@ -307,6 +307,23 @@ class Menu {
$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);
}

View File

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

View File

@ -68,7 +68,7 @@ class FranksRoastHouseTemplate {
"link" => "http://www.example.com",
"src" => $this->template_image_url . "/header-v2.jpg",
"alt" => __("Frank's Roast House"),
"padded" => false,
"fullWidth" => true,
"width" => "600px",
"height" => "220px",
"styles" => array(
@ -95,7 +95,7 @@ class FranksRoastHouseTemplate {
"link" => "http://example.org",
"src" => $this->template_image_url . "/coffee-grain.jpg",
"alt" => __("coffee-grain-3-1329675-1599x941"),
"padded" => true,
"fullWidth" => false,
"width" => "1599px",
"height" => "777px",
"styles" => array(
@ -139,7 +139,7 @@ class FranksRoastHouseTemplate {
"link" => "http://example.org",
"src" => $this->template_image_url . "/sandwich.jpg",
"alt" => "sandwich",
"padded" => true,
"fullWidth" => false,
"width" => "640px",
"height" => "344px",
"styles" => array(
@ -238,7 +238,7 @@ class FranksRoastHouseTemplate {
"link" => "http://example.org",
"src" => $this->template_image_url . "/map-v2.jpg",
"alt" => __("map-v2"),
"padded" => true,
"fullWidth" => false,
"width" => "636px",
"height" => "342px",
"styles" => array(
@ -279,19 +279,19 @@ class FranksRoastHouseTemplate {
"blocks" => array(
array(
"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(
"block" => array(
"backgroundColor" => "#a9a7a7"
),
"text" => array(
"fontColor" => "#ffffff",
"fontColor" => "#000000",
"fontFamily" => "Arial",
"fontSize" => "12px",
"textAlign" => "center"
),
"link" => array(
"fontColor" => "#ffffff",
"fontColor" => "#000000",
"textDecoration" => "underline"
)
)

View File

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

View File

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

View File

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

View File

@ -9,13 +9,12 @@ class Widget {
}
function init() {
add_action('widgets_init', array($this, 'registerWidget'));
$this->registerWidget();
if(!is_admin()) {
//$this->setupActions();
add_action('widgets_init', array($this, 'setupDependencies'));
$this->setupDependencies();
} 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() {
// ajax requests
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;
use MailPoet\Cron\Workers\SendingQueue;
use MailPoet\Models\Setting;
use MailPoet\Util\Security;
require_once(ABSPATH . 'wp-includes/pluggable.php');
@ -13,132 +11,69 @@ class Daemon {
public $daemon;
public $request_payload;
public $refreshed_token;
public $timer;
const daemon_request_timeout = 5;
private $timer;
function __construct($request_payload = array()) {
set_time_limit(0);
ignore_user_abort();
$this->daemon = $this->getDaemon();
$this->refreshed_token = $this->refreshToken();
$this->daemon = CronHelper::getDaemon();
$this->token = CronHelper::createToken();
$this->request_payload = $request_payload;
$this->timer = microtime(true);
}
function start() {
if(!isset($this->request_payload['session'])) {
$this->abortWithError(__('Missing session ID.'));
}
$this->manageSession('start');
function run() {
$daemon = $this->daemon;
if(!$daemon) {
$this->saveDaemon(
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.'));
$this->abortWithError(__('Daemon does not exist.'));
}
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 {
$sending_queue = new SendingQueue($this->timer);
$sending_queue->process();
} catch(Exception $e) {
}
$elapsed_time = microtime(true) - $this->timer;
if($elapsed_time < 30) {
sleep(30 - $elapsed_time);
if($elapsed_time < CronHelper::daemon_execution_limit) {
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']++;
$this->saveDaemon($daemon);
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;
$this->abortIfStopped($daemon);
if($daemon['status'] === 'starting') {
$daemon['status'] = 'started';
}
$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() {
$payload = json_encode(array('token' => $this->refreshed_token));
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)
));
CronHelper::accessDaemon($this->token, self::daemon_request_timeout);
exit;
}
}

View File

@ -1,103 +1,94 @@
<?php
namespace MailPoet\Cron;
use MailPoet\Config\Env;
use MailPoet\Models\Setting;
if(!defined('ABSPATH')) exit;
class Supervisor {
public $daemon;
public $token;
public $force_run;
function __construct($force_start = false) {
$this->force_start = $force_start;
if(!Env::isPluginActivated()) {
throw new \Exception(__('MailPoet is not activated.'));
}
$this->daemon = $this->getDaemon();
function __construct($force_run = false) {
$this->daemon = CronHelper::getDaemon();
$this->token = CronHelper::createToken();
$this->force_run = $force_run;
}
function checkDaemon() {
if(!$this->daemon) {
return $this->startDaemon();
$daemon = $this->daemon;
if(!$daemon) {
$daemon = CronHelper::createDaemon($this->token);
return $this->runDaemon($daemon);
}
if(
!$this->force_start &&
in_array($this->daemon['status'], array('stopped', 'stopping'))
// if the daemon is stopped, return its status and do nothing
if(!$this->force_run &&
isset($daemon['status']) &&
$daemon['status'] === 'stopped'
) {
return $this->daemon['status'];
return $this->formatDaemonStatusMessage($daemon['status']);
}
$elapsed_time = time() - (int)$this->daemon['updated_at'];
if($elapsed_time < 40) {
if(!$this->force_start) {
return;
}
if($this->daemon['status'] === 'stopping' ||
$this->daemon['status'] === 'starting'
) {
return $this->daemon['status'];
}
$elapsed_time = time() - (int) $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 < CronHelper::daemon_execution_timeout && !$this->force_run) {
return $this->formatDaemonStatusMessage($daemon['status']);
}
$this->daemon['status'] = 'starting';
$this->saveDaemon($this->daemon);
return $this->startDaemon();
// if it's been less than 40 seconds since last execution, we are
// force-running the daemon and it's either being started or stopped,
// 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() {
if(!session_id()) session_start();
$sessionId = session_id();
session_write_close();
$_SESSION['cron_daemon'] = null;
$requestPayload = json_encode(array('session' => $sessionId));
self::accessRemoteUrl(
'/?mailpoet-api&section=queue&action=start&request_payload=' .
urlencode($requestPayload)
);
session_start();
$daemonStatus = $_SESSION['cron_daemon'];
unset($_SESSION['daemon']);
session_write_close();
return $daemonStatus;
function runDaemon() {
$request = CronHelper::accessDaemon($this->token);
preg_match('/\[(mailpoet_cron_error:.*?)\]/i', $request, $status);
$daemon = CronHelper::getDaemon();
if(!empty($status) || !$daemon) {
if(!$daemon) {
$message = __('Daemon failed to run.');
} else {
list(, $message) = explode(':', $status[0]);
$message = base64_decode($message);
}
return $this->formatResultMessage(
false,
$message
);
}
return $this->formatDaemonStatusMessage($daemon['status']);
}
function getDaemon() {
return Setting::getValue('cron_daemon');
}
function saveDaemon($daemon_data) {
return Setting::setValue(
'cron_daemon',
$daemon_data
private function formatDaemonStatusMessage($status) {
return $this->formatResultMessage(
true,
sprintf(
__('Daemon is currently %s.'),
__($status)
)
);
}
static function accessRemoteUrl($url) {
$args = array(
'timeout' => 1,
'user-agent' => 'MailPoet (www.mailpoet.com) Cron'
private function formatResultMessage($result, $message) {
$formattedResult = array(
'result' => $result
);
wp_remote_get(
self::getSiteUrl() . $url,
$args
);
}
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.'));
if(!$result) {
$formattedResult['errors'] = array($message);
} else {
$formattedResult['message'] = $message;
}
return $formattedResult;
}
}

View File

@ -1,123 +1,190 @@
<?php
namespace MailPoet\Cron\Workers;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterStatistics;
use MailPoet\Models\Setting;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class SendingQueue {
public $timer;
public $mta_config;
public $mta_log;
public $processing_method;
private $timer;
const batch_size = 50;
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);
}
function process() {
// TODO: implement mailer sending frequency limits
foreach($this->getQueues() as $queue) {
$newsletter = Newsletter::findOne($queue->newsletter_id)
->asArray();
$newsletter = Newsletter::findOne($queue->newsletter_id);
if(!$newsletter) {
continue;
};
$mailer = $this->configureMailerForNewsletter($newsletter);
$newsletter = $this->renderNewsletter($newsletter);
$subscribers = json_decode($queue->subscribers, true);
$subscribers_to_process = $subscribers['to_process'];
if(!isset($subscribers['processed'])) $subscribers['processed'] = array();
if(!isset($subscribers['failed'])) $subscribers['failed'] = array();
foreach(array_chunk($subscribers_to_process, 200) as $subscriber_ids) {
$db_subscribers = Subscriber::whereIn('id', $subscriber_ids)
}
$queue->subscribers = (object) unserialize($queue->subscribers);
if(!isset($queue->subscribers->processed)) {
$queue->subscribers->processed = array();
}
if(!isset($queue->subscribers->failed)) {
$queue->subscribers->failed = array();
}
$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();
foreach($db_subscribers as $db_subscriber) {
$this->checkExecutionTimer();
$result = $this->sendNewsletter(
$queue->subscribers = call_user_func_array(
array(
$this,
$this->processing_method
),
array(
$mailer,
$this->processNewsletter($newsletter),
$db_subscriber);
if($result) {
$this->updateStatistics($newsletter['id'], $db_subscriber['id'], $queue->id);
$subscribers['processed'][] = $db_subscriber['id'];
} else {
$subscribers['failed'][] = $db_subscriber['id'];
}
$this->updateQueue($queue, $subscribers);
}
$newsletter,
$subscribers,
$queue
)
);
}
}
}
function processNewsletter($newsletter) {
// TODO: replace shortcodes, etc..
function processBulkSubscribers($mailer, $newsletter, $subscribers, $queue) {
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;
}
function sendNewsletter($mailer, $newsletter, $subscriber) {
return $mailer->mailer_instance->send(
$newsletter,
$mailer->transformSubscriber($subscriber)
$subscriber
);
}
function updateStatistics($newsletter_id, $subscriber_id, $queue_id) {
$newsletter_statistic = NewsletterStatistics::create();
$newsletter_statistic->subscriber_id = $newsletter_id;
$newsletter_statistic->newsletter_id = $subscriber_id;
$newsletter_statistic->queue_id = $queue_id;
$newsletter_statistic->save();
}
function updateQueue($queue, $subscribers) {
$subscribers['to_process'] = array_values(
array_diff(
$subscribers['to_process'],
array_merge($subscribers['processed'], $subscribers['failed'])
)
);
$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 {
function configureMailer($newsletter) {
$sender['address'] = (!empty($newsletter['sender_address'])) ?
$newsletter['sender_address'] :
false;
$sender['name'] = (!empty($newsletter['sender_name'])) ?
$newsletter['sender_name'] :
false;
$reply_to['address'] = (!empty($newsletter['reply_to_address'])) ?
$newsletter['reply_to_address'] :
false;
$reply_to['name'] = (!empty($newsletter['reply_to_name'])) ?
$newsletter['reply_to_name'] :
false;
if(!$sender['address']) {
$sender = false;
}
if(!empty($newsletter['reply_to_address']) && !empty($newsletter['reply_to_name'])) {
$reply_to = array(
'name' => $newsletter['reply_to_name'],
'address' => $newsletter['reply_to_address']
);
} else {
if(!$reply_to['address']) {
$reply_to = false;
}
$mailer = new Mailer($method = false, $sender, $reply_to);
return $mailer;
}
function checkExecutionTimer() {
$elapsed_time = microtime(true) - $this->timer;
if($elapsed_time >= 30) throw new \Exception('Maximum execution time reached.');
}
function getQueues() {
return \MailPoet\Models\SendingQueue::orderByDesc('priority')
->whereNull('deleted_at')
@ -125,10 +192,78 @@ class SendingQueue {
->findResultSet();
}
function renderNewsletter($newsletter) {
$renderer = new Renderer(json_decode($newsletter['body'], true));
// TODO: update once text rendering is implemented/enderer returns an array
$newsletter['body'] = array('html' => $renderer->render(), 'text' => '');
return $newsletter;
function updateQueue($queue) {
$queue = clone($queue);
$queue->subscribers->to_process = array_diff(
$queue->subscribers->to_process,
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') {
$rules['required'] = true;
$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'])) {
@ -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['errors-container'] = '.mailpoet_error_'.$block['id'];
$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_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 .= 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 .= '<input type="checkbox" class="mailpoet_checkbox" ';
@ -35,6 +36,8 @@ class Checkbox extends Base {
$html .= '</label>';
}
$html .= '<span class="mailpoet_error_'.$block['id'].'"></span>';
$html .= '</p>';
return $html;

View File

@ -13,9 +13,12 @@ class Radio extends Base {
$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 .= '<input type="radio" class="mailpoet_radio" ';
@ -33,6 +36,8 @@ class Radio extends Base {
$html .= '</label>';
}
$html .= '<span class="mailpoet_error_'.$block['id'].'"></span>';
$html .= '</p>';
return $html;

View File

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

View File

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

View File

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

View File

@ -49,7 +49,7 @@ class Renderer {
private static function renderBlock($block = array()) {
$html = '';
switch ($block['type']) {
switch($block['type']) {
case 'html':
$html .= Block\Html::render($block);
break;
@ -78,8 +78,8 @@ class Renderer {
$html .= Block\Select::render($block);
break;
case 'input':
$html .= Block\Input::render($block);
case 'text':
$html .= Block\Text::render($block);
break;
case 'textarea':

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
$title = isset($instance['title']) ? strip_tags($instance['title']) : '';
@ -102,8 +104,9 @@ class Widget extends \WP_Widget {
endpoint: 'forms',
action: 'create'
}).done(function(response) {
if(response !== false) {
window.location = response;
if(response.result && response.form_id) {
window.location =
"<?php echo $form_edit_url; ?>" + response.form_id;
}
});
return false;

View File

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

View File

@ -13,25 +13,27 @@ class AmazonSES {
public $aws_termination_string;
public $hash_algorithm;
public $url;
public $from;
public $sender;
public $reply_to;
public $date;
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_secret_key = $secret_key;
$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_service = 'ses';
$this->aws_termination_string = 'aws4_request';
$this->hash_algorithm = 'sha256';
$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_without_time = gmdate('Ymd');
}
function send($newsletter, $subscriber) {
$result = wp_remote_post(
$this->url,
@ -42,15 +44,16 @@ class AmazonSES {
wp_remote_retrieve_response_code($result) === 200
);
}
function getBody($newsletter, $subscriber) {
$body = array(
'Action' => 'SendEmail',
'Version' => '2010-12-01',
'Source' => $this->from,
'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'],
'ReturnPath' => $this->from
'ReturnPath' => $this->sender['from_name_email'],
);
if(!empty($newsletter['body']['html'])) {
$body['Message.Body.Html.Data'] = $newsletter['body']['html'];
@ -60,7 +63,7 @@ class AmazonSES {
}
return $body;
}
function request($newsletter, $subscriber) {
$body = $this->getBody($newsletter, $subscriber);
return array(
@ -75,7 +78,7 @@ class AmazonSES {
'body' => urldecode(http_build_query($body))
);
}
function signRequest($body) {
$string_to_sign = $this->createStringToSign(
$this->getCredentialScope(),
@ -86,7 +89,7 @@ class AmazonSES {
$string_to_sign,
$this->getSigningKey()
);
return sprintf(
'%s Credential=%s/%s, SignedHeaders=host;x-amz-date, Signature=%s',
$this->aws_signing_algorithm,
@ -94,7 +97,7 @@ class AmazonSES {
$this->getCredentialScope(),
$signature);
}
function getCredentialScope() {
return sprintf(
'%s/%s/%s/%s',
@ -103,7 +106,7 @@ class AmazonSES {
$this->aws_service,
$this->aws_termination_string);
}
function getCanonicalRequest($body) {
return implode("\n", array(
'POST',
@ -116,7 +119,7 @@ class AmazonSES {
hash($this->hash_algorithm, urldecode(http_build_query($body)))
));
}
function createStringToSign($credential_scope, $canonical_request) {
return implode("\n", array(
$this->aws_signing_algorithm,
@ -125,7 +128,7 @@ class AmazonSES {
hash($this->hash_algorithm, $canonical_request)
));
}
function getSigningKey() {
$date_key = hash_hmac(
$this->hash_algorithm,

View File

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

View File

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

View File

@ -6,26 +6,27 @@ if(!defined('ABSPATH')) exit;
class MailPoet {
public $url = 'https://bridge.mailpoet.com/api/messages';
public $api_key;
public $from_email;
public $from_name;
function __construct($api_key, $from_email, $from_name) {
public $sender;
public $reply_to;
function __construct($api_key, $sender, $reply_to) {
$this->api_key = $api_key;
$this->from_email = $from_email;
$this->from_name = $from_name;
$this->sender = $sender;
$this->reply_to = $reply_to;
}
function send($newsletter, $subscriber) {
$message_body = $this->getBody($newsletter, $subscriber);
$result = wp_remote_post(
$this->url,
$this->request($newsletter, $this->processSubscriber($subscriber))
$this->request($message_body)
);
return (
!is_wp_error($result) === true &&
wp_remote_retrieve_response_code($result) === 201
);
}
function processSubscriber($subscriber) {
preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriber, $subscriber_data);
if(!isset($subscriber_data['email'])) {
@ -38,34 +39,51 @@ class MailPoet {
'name' => (isset($subscriber_data['name'])) ? $subscriber_data['name'] : ''
);
}
function getBody($newsletter, $subscriber) {
$body = array(
'to' => (array(
'address' => $subscriber['email'],
'name' => $subscriber['name']
)),
'from' => (array(
'address' => $this->from_email,
'name' => $this->from_name
)),
'subject' => $newsletter['subject']
);
if(!empty($newsletter['body']['html'])) {
$body['html'] = $newsletter['body']['html'];
}
if(!empty($newsletter['body']['text'])) {
$body['text'] = $newsletter['body']['text'];
$composeBody = function ($newsletter, $subscriber) {
$body = array(
'to' => (array(
'address' => $subscriber['email'],
'name' => $subscriber['name']
)),
'from' => (array(
'address' => $this->sender['from_email'],
'name' => $this->sender['from_name']
)),
'reply_to' => (array(
'address' => $this->reply_to['reply_to_email'],
'name' => $this->reply_to['reply_to_name']
)),
'subject' => $newsletter['subject']
);
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;
}
function auth() {
return 'Basic ' . base64_encode('api:' . $this->api_key);
}
function request($newsletter, $subscriber) {
$body = array($this->getBody($newsletter, $subscriber));
function request($body) {
return array(
'timeout' => 10,
'httpversion' => '1.0',
@ -74,7 +92,7 @@ class MailPoet {
'Content-Type' => 'application/json',
'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 $password;
public $encryption;
public $from_name;
public $from_email;
public $sender;
public $reply_to;
public $mailer;
function __construct(
$host, $port, $authentication, $login = null, $password = null, $encryption,
$from_email, $from_name) {
$sender, $reply_to) {
$this->host = $host;
$this->port = $port;
$this->authentication = $authentication;
$this->login = $login;
$this->password = $password;
$this->encryption = $encryption;
$this->from_name = $from_name;
$this->from_email = $from_email;
$this->sender = $sender;
$this->reply_to = $reply_to;
$this->mailer = $this->buildMailer();
}
@ -50,11 +50,15 @@ class SMTP {
return \Swift_Mailer::newInstance($transport);
}
function createMessage($newsletter, $subscriber) {
$message = \Swift_Message::newInstance()
->setFrom(array($this->from_email => $this->from_name))
->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');

View File

@ -6,13 +6,13 @@ if(!defined('ABSPATH')) exit;
class SendGrid {
public $url = 'https://api.sendgrid.com/api/mail.send.json';
public $api_key;
public $from_email;
public $from_name;
public $sender;
public $reply_to;
function __construct($api_key, $from_email, $from_name) {
function __construct($api_key, $sender, $reply_to) {
$this->api_key = $api_key;
$this->from_email = $from_email;
$this->from_name = $from_name;
$this->sender = $sender;
$this->reply_to = $reply_to;
}
function send($newsletter, $subscriber) {
@ -20,10 +20,11 @@ class SendGrid {
$this->url,
$this->request($newsletter, $subscriber)
);
$result_body = json_decode($result['body'], true);
return (
!is_wp_error($result) === 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
);
}
@ -31,8 +32,9 @@ class SendGrid {
function getBody($newsletter, $subscriber) {
$body = array(
'to' => $subscriber,
'from' => $this->from_email,
'from_name' => $this->from_name,
'from' => $this->sender['from_email'],
'fromname' => $this->sender['from_name'],
'replyto' => $this->reply_to['reply_to_email'],
'subject' => $newsletter['subject']
);
if(!empty($newsletter['body']['html'])) {
@ -57,7 +59,7 @@ class SendGrid {
'headers' => array(
'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() {
$model = parent::asArray();
$model['params'] = (
is_serialized($this->params)
? unserialize($this->params)
: $this->params
);
if(isset($model['params'])) {
$model['params'] = (is_array($this->params))
? $this->params
: unserialize($this->params);
}
return $model;
}
function save() {
if(is_null($this->params)) {
$this->params = array();
}
$this->set('params', (
is_serialized($this->params)
? $this->params
: serialize($this->params)
is_array($this->params)
? serialize($this->params)
: $this->params
));
return parent::save();
}
@ -54,7 +56,7 @@ class CustomField extends Model {
}
// set name as label by default
if(empty($data['params']['label'])) {
if(empty($data['params']['label']) && isset($data['name'])) {
$data['params']['label'] = $data['name'];
}
@ -66,12 +68,6 @@ class CustomField extends Model {
$custom_field->set($data);
}
try {
$custom_field->save();
return $custom_field;
} catch(Exception $e) {
return $custom_field->getValidationErrors();
}
return false;
return $custom_field->save();
}
}

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

View File

@ -4,25 +4,48 @@ namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class Model extends \Sudzy\ValidModel {
protected $_errors;
function __construct() {
$customValidators = new CustomValidator();
parent::__construct($customValidators->init());
$this->_errors = array();
parent::__construct();
}
static function 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() {
$this->setTimestamp();
try {
parent::save();
return true;
} catch (\Sudzy\ValidationException $e) {
return array_unique($e->getValidationErrors());
} catch (\PDOException $e) {
return $e->getMessage();
} catch(\Sudzy\ValidationException $e) {
$this->setError($e->getValidationErrors());
} catch(\PDOException $e) {
$this->setError($e->getMessage());
} catch(\Exception $e) {
$this->setError($e->getMessage());
}
return $this;
}
function trash() {

View File

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

View File

@ -9,4 +9,16 @@ class NewsletterStatistics extends Model {
function __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()) {
$template = false;
@ -32,16 +40,7 @@ class NewsletterTemplate extends Model {
$template->set($data);
}
$saved = $template->save();
if($saved === true) {
return true;
} else {
$errors = $template->getValidationErrors();
if(!empty($errors)) {
return $errors;
}
}
return false;
$template->save();
return $template;
}
}

View File

@ -164,8 +164,7 @@ class Segment extends Model {
$segment->set($data);
}
$segment->save();
return $segment;
return $segment->save();
}
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;
} else {
$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();
} else {
$this->set_expr('status', 'NULL');
return $this->save();
$this->save();
return ($this->getErrors() === false && $this->id() > 0);
}
}
function complete() {
$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();
$this->addValidations('name', array(
'required' => 'name_is_blank',
'isString' => 'name_is_not_string'
'required' => __('You need to specify a name.')
));
}
@ -36,6 +35,9 @@ class Setting extends Model {
if($setting !== $default) {
for($i = 0, $count = count($keys); $i < $count; $i++) {
if(!is_array($setting)) {
$setting = array();
}
if(array_key_exists($keys[$i], $setting)) {
$setting = $setting[$keys[$i]];
} else {
@ -55,10 +57,11 @@ class Setting extends Model {
$value = serialize($value);
}
return Setting::createOrUpdate(array(
$setting = Setting::createOrUpdate(array(
'name' => $key,
'value' => $value
));
return ($setting->id() > 0 && $setting->getErrors() === false);
} else {
$main_key = array_shift($keys);
@ -67,13 +70,6 @@ class Setting extends Model {
$last_key = array_pop($keys);
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];
}
if(is_scalar($current_value)) {
@ -100,17 +96,20 @@ class Setting extends Model {
return $settings;
}
public static function createOrUpdate($model) {
$exists = self::where('name', $model['name'])
->find_one();
public static function createOrUpdate($data = array()) {
$setting = false;
if($exists === false) {
$new_model = self::create();
$new_model->hydrate($model);
return $new_model->save();
if(isset($data['name'])) {
$setting = self::where('name', $data['name'])->findOne();
}
$exists->value = $model['value'];
return $exists->save();
if($setting === false) {
$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()) {
$segments = Segment::whereIn('id', $segment_ids)->findMany();
foreach($segments as $segment) {
$association = SubscriberSegment::create();
$association->subscriber_id = $this->id;
$association->segment_id = $segment->id;
$association->save();
// delete all relations to segments
SubscriberSegment::where('subscriber_id', $this->id)->deleteMany();
if(!empty($segment_ids)) {
$segments = Segment::whereIn('id', $segment_ids)->findMany();
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;
}
$subscriber = static::createOrUpdate($subscriber_data);
$subscriber = self::createOrUpdate($subscriber_data);
if($subscriber !== false && $subscriber->id() > 0) {
// restore deleted subscriber
@ -92,20 +97,25 @@ class Subscriber extends Model {
$segments = Segment::orderByAsc('name')->findMany();
$segment_list = array();
$segment_list[] = array(
'label' => __('All lists'),
'label' => __('All segments'),
'value' => ''
);
$segment_list[] = array(
'label' => sprintf(
__('Subscribers without a segment (%d)'),
self::filter('withoutSegments')->count()
),
'value' => 'none'
);
foreach($segments as $segment) {
$subscribers_count = $segment->subscribers()
->whereNull('deleted_at')
->count();
if($subscribers_count > 0) {
$segment_list[] = array(
'label' => sprintf('%s (%d)', $segment->name, $subscribers_count),
'value' => $segment->id()
);
}
$segment_list[] = array(
'label' => sprintf('%s (%d)', $segment->name, $subscribers_count),
'value' => $segment->id()
);
}
$filters = array(
@ -121,9 +131,13 @@ class Subscriber extends Model {
}
foreach($filters as $key => $value) {
if($key === 'segment') {
$segment = Segment::findOne($value);
if($segment !== false) {
return $segment->subscribers();
if($value === 'none') {
return self::filter('withoutSegments');
} else {
$segment = Segment::findOne($value);
if($segment !== false) {
return $segment->subscribers();
}
}
}
}
@ -135,27 +149,27 @@ class Subscriber extends Model {
array(
'name' => 'all',
'label' => __('All'),
'count' => Subscriber::getPublished()->count()
'count' => self::getPublished()->count()
),
array(
'name' => 'subscribed',
'label' => __('Subscribed'),
'count' => Subscriber::filter('subscribed')->count()
'count' => self::filter('subscribed')->count()
),
array(
'name' => 'unconfirmed',
'label' => __('Unconfirmed'),
'count' => Subscriber::filter('unconfirmed')->count()
'count' => self::filter('unconfirmed')->count()
),
array(
'name' => 'unsubscribed',
'label' => __('Unsubscribed'),
'count' => Subscriber::filter('unsubscribed')->count()
'count' => self::filter('unsubscribed')->count()
),
array(
'name' => 'trash',
'label' => __('Trash'),
'count' => Subscriber::getTrashed()->count()
'count' => self::getTrashed()->count()
)
);
}
@ -226,28 +240,91 @@ class Subscriber extends Model {
$subscriber = false;
if(isset($data['id']) && (int)$data['id'] > 0) {
$subscriber = static::findOne((int)$data['id']);
$subscriber = self::findOne((int)$data['id']);
unset($data['id']);
}
if($subscriber === false && !empty($data['email'])) {
$subscriber = static::where('email', $data['email'])->findOne();
$subscriber = self::where('email', $data['email'])->findOne();
if($subscriber !== false) {
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) {
$subscriber = static::create();
$subscriber = self::create();
$subscriber->hydrate($data);
} else {
$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;
}
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()) {
$segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0);
$segment = Segment::findOne($segment_id);
@ -376,6 +453,19 @@ class Subscriber extends Model {
->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) {
return self::rawExecute(
'INSERT INTO `' . self::$_table . '` ' .

View File

@ -12,6 +12,49 @@ class SubscriberCustomField extends Model {
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) {
$values = array_map('array_values', $values);
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);
$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(
$post,
$this->args['displayType'],
$this->args['imagePadded'] === 'true',
$this->args['imageFullWidth'] === 'true',
$structure
);
$structure = $this->appendPostTitle($post, $structure);
@ -36,7 +36,7 @@ class PostTransformer {
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') {
// No featured images for full posts
return $structure;
@ -45,7 +45,7 @@ class PostTransformer {
$featured_image = $this->getFeaturedImage(
$post->ID,
$post->post_title,
(bool)$image_padded
(bool)$image_full_width
);
if (is_array($featured_image)) {
@ -55,14 +55,14 @@ class PostTransformer {
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)) {
$thumbnail_id = get_post_thumbnail_id($post_id);
// get attachment data (src, width, height)
$image_info = wp_get_attachment_image_src(
$thumbnail_id,
'single-post-thumbnail'
'mailpoet_newsletter_max'
);
// get alt text
@ -81,7 +81,7 @@ class PostTransformer {
'link' => get_permalink($post_id),
'src' => $image_info[0],
'alt' => $alt_text,
'padded' => $image_padded,
'fullWidth' => $image_full_width,
'width' => $image_info[1],
'height' => $image_info[2],
'styles' => array(

View File

@ -7,11 +7,11 @@ if(!defined('ABSPATH')) exit;
class StructureTransformer {
function transform($content, $image_padded) {
function transform($content, $image_full_width) {
$root = pQuery::parseStr($content);
$this->hoistImagesToRoot($root);
$structure = $this->transformTagsToBlocks($root, $image_padded);
$structure = $this->transformTagsToBlocks($root, $image_full_width);
$structure = $this->mergeNeighboringBlocks($structure);
return $structure;
}
@ -44,8 +44,8 @@ class StructureTransformer {
* Transforms HTML tags into their respective JSON objects,
* turns other root children into text blocks
*/
private function transformTagsToBlocks($root, $image_padded) {
return array_map(function($item) use ($image_padded) {
private function transformTagsToBlocks($root, $image_full_width) {
return array_map(function($item) use ($image_full_width) {
if ($item->tag === 'img' || $item->tag === 'a' && $item->query('img')) {
if ($item->tag === 'a') {
$link = $item->getAttribute('href');
@ -60,7 +60,7 @@ class StructureTransformer {
'link' => $link,
'src' => $image->getAttribute('src'),
'alt' => $image->getAttribute('alt'),
'padded' => $image_padded,
'fullWidth' => $image_full_width,
'width' => $image->getAttribute('width'),
'height' => $image->getAttribute('height'),
'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 {
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('/(<\/?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 = '
<tr>
<td class="mailpoet_padded_header_footer mailpoet_footer" bgcolor="' . $element['styles']['block']['backgroundColor'] . '"
style="' . StylesHelper::getBlockStyles($element) . StylesHelper::getStyles($element['styles'], 'text') . '">
' . $element['text'] . '
' . $DOM->html() . '
</td>
</tr>';
return $template;

View File

@ -5,21 +5,23 @@ use MailPoet\Newsletter\Renderer\StylesHelper;
class Header {
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('/(<\/?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 = '
<tr>
<td class="mailpoet_padded_header_footer mailpoet_header" bgcolor="' . $element['styles']['block']['backgroundColor'] . '"
style="' . StylesHelper::getBlockStyles($element) . StylesHelper::getStyles($element['styles'], 'text') . '">
' . $element['text'] . '
' . $DOM->html() . '
</td>
</tr>';
return $template;

View File

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

View File

@ -6,10 +6,9 @@ class Text {
$html = $element['text'];
$html = self::convertBlockquotesToTables($html);
$html = self::convertParagraphsToTables($html);
$html = self::addLineBreakAfterTags($html);
$html = self::styleLists($html);
$html = self::styleHeadings($html);
$html = self::removeLastElementBreakLine($html);
$html = self::addLineBreakAfterTags($html);
$template = '
<tr>
<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;
}
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) {
$template = '
<table width="100%" cellpadding="0" cellspacing="0" border="0">
$DOM_parser = new \pQuery();
$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>
<tr>
<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">
<tr>
<td class="mailpoet_blockquote">
$1
' . implode('', $contents) . '
</td>
</tr>
</table>
</td>
</tr>
</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
</tbody>'
);
}
return $html;
return $DOM->__toString();
}
static function styleHeadings($html) {
return preg_replace(
'/<(h[1-6])(?:.+style=\"(.*)?\")?>/',
'<$1 style="margin:0;font-style:normal;font-weight:normal;$2">',
$html
);
static function convertParagraphsToTables($html) {
$DOM_parser = new \pQuery();
$DOM = $DOM_parser->parseStr($html);
$paragraphs = $DOM->query('p');
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) {
$html = preg_replace(
'/<(ul|ol)>/',
'<$1 class="mailpoet_paragraph" style="padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;">',
$html
);
$html = preg_replace('/<li>/', '<li class="mailpoet_paragraph">', $html);
return $html;
$DOM_parser = new \pQuery();
$DOM = $DOM_parser->parseStr($html);
$lists = $DOM->query('ol, ul, li');
if(!$lists->count()) return $html;
foreach($lists as $list) {
if($list->tag === 'li') {
$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
));
$template = $this->inlineCSSStyles($template);
return $this->postProcessTemplate($template);
$template = $this->postProcessTemplate($template);
return array(
'html' => $template,
'text' => $this->renderTextVersion($template)
);
}
function renderBody($content) {
@ -65,7 +69,7 @@ class Renderer {
$selector = 'h3';
break;
case 'text':
$selector = '.mailpoet_paragraph, .mailpoet_blockquote';
$selector = '.mailpoet_paragraph, td.mailpoet_blockquote';
break;
case 'body':
$selector = 'body, .mailpoet-wrapper';
@ -107,8 +111,8 @@ class Renderer {
}
function renderTextVersion($template) {
// TODO: add text rendering
return $template;
$template = mb_convert_encoding($template, 'HTML-ENTITIES', 'UTF-8');
return \Html2Text\Html2Text::convert($template);
}
function postProcessTemplate($template) {
@ -118,7 +122,6 @@ class Renderer {
$template->html(
str_replace('!important', '', $template->html())
);
// TODO: return array with html and text body
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);
}
}

View File

@ -2,7 +2,7 @@
namespace MailPoet\Router;
use Carbon\Carbon;
use MailPoet\Cron\Daemon;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Supervisor;
use MailPoet\Models\Setting;
@ -10,35 +10,27 @@ if(!defined('ABSPATH')) exit;
class Cron {
function start() {
$supervisor = new Supervisor($forceStart = true);
wp_send_json(
array(
'result' => $supervisor->checkDaemon() ? true : false
)
);
$supervisor = new Supervisor($force_run = true);
return $supervisor->checkDaemon();
}
function stop() {
$daemon = new Daemon();
if(!$daemon->daemon ||
$daemon->daemon['status'] !== 'started'
) {
$daemon = CronHelper::getDaemon();
if(!$daemon || $daemon['status'] !== 'started') {
$result = false;
} else {
$daemon->daemon['status'] = 'stopping';
$result = $daemon->saveDaemon($daemon->daemon);
$daemon['status'] = 'stopping';
$result = CronHelper::saveDaemon($daemon);
}
wp_send_json(
array(
'result' => $result
)
return array(
'result' => $result
);
}
function getStatus() {
$daemon = Setting::where('name', 'cron_daemon')
->findOne();
wp_send_json(
return (
($daemon) ?
array_merge(
array(

View File

@ -14,56 +14,46 @@ class CustomFields {
return $custom_field->asArray();
}, $collection);
wp_send_json($custom_fields);
return $custom_fields;
}
function delete($id) {
$result = false;
$custom_field = CustomField::findOne($id);
if($custom_field !== false) {
if($custom_field === false or !$custom_field->id()) {
return array('result' => false);
} else {
$custom_field->delete();
$result = true;
}
wp_send_json($result);
return array(
'result' => true,
'field' => $custom_field->asArray()
);
}
}
function save($data = array()) {
$custom_field = CustomField::createOrUpdate($data);
$errors = $custom_field->getErrors();
if($custom_field === false) {
$result = array(
if(!empty($errors)) {
return array(
'result' => false,
'errors' => array(
__('The custom field could not be created.')
)
'errors' => $errors
);
} else {
$errors = $custom_field->getValidationErrors();
if(!empty($errors)) {
$result = array(
'result' => false,
'errors' => $errors
);
} else {
$result = array(
'result' => true,
'field' => $custom_field->asArray()
);
}
return array(
'result' => true,
'field' => $custom_field->asArray()
);
}
wp_send_json($result);
}
function get($id) {
$custom_field = CustomField::findOne($id);
if($custom_field === false) {
wp_send_json(false);
return false;
} else {
$custom_field = $custom_field->asArray();
wp_send_json($custom_field);
return $custom_field->asArray();
}
}
}

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