Compare commits

...

62 Commits

Author SHA1 Message Date
Tautvidas Sipavičius
b2d4bfc760 Initial MailPoet 3.0.0-beta.1 release 2016-10-28 13:52:40 +03:00
Tautvidas Sipavičius
57f5f16bb6 Merge pull request #674 from mailpoet/premium_hook
Bypasses subscriber count enforcement for premium users
2016-10-27 20:47:14 +03:00
Vlad
7d2e13b9a3 - Updates license check logic
- Updates subscriber limit check logic
- Updates unit tests
- Updates Menu's check for subscriber limit
2016-10-27 12:35:57 -04:00
mrcasual
6d39f9fa78 Merge pull request #671 from mailpoet/plugin_repository_assets
Preparation for plugin repository
2016-10-27 11:36:57 -04:00
Vlad
a4395f2350 - Adds unit tests 2016-10-27 11:16:30 -04:00
Vlad
411969b3eb - Adds check for premium plugin status
- Bypasses subscriber count enforcement if premium is enabled
2016-10-27 10:20:05 -04:00
Tautvidas Sipavičius
1868ca3155 Merge pull request #673 from mailpoet/scheduler_update
Scheduler update
2016-10-27 13:17:03 +03:00
Vlad
e765471f5d - Changes month days count to start from 1 instead of 0
- Closes #672
2016-10-26 21:18:07 -04:00
Vlad
bdce7c5e5a - Remove unused dependency 2016-10-26 11:43:32 -04:00
Tautvidas Sipavičius
773be9f5c8 Add assets for plugin repository 2016-10-26 13:59:02 +03:00
Tautvidas Sipavičius
6ae46b05e5 Merge pull request #669 from mailpoet/string_updates
String updates
2016-10-25 18:00:44 +03:00
Vlad
217894745d - Updates text strings
- Closes #655
2016-10-25 10:21:23 -04:00
Tautvidas Sipavičius
a03891895c Bump up release version to 0.0.50 and update changelog 2016-10-25 13:04:55 +03:00
Tautvidas Sipavičius
3368e84a99 Merge pull request #668 from mailpoet/export_confirmed_subscribers_option_update
Fixes minor export UI issues
2016-10-25 12:46:13 +03:00
Vlad
e90df2f08d - Fixes Select2 not dislaying multiple options in the list of export
fields
- Sets default "export confirmed subscriber" option to "no"
2016-10-24 13:09:48 -04:00
Tautvidas Sipavičius
2391ae1cad Merge pull request #665 from mailpoet/post_notification_fix
Fixes post notification issues
2016-10-24 16:02:19 +03:00
Vlad
83114a8be4 - Removes unused class declarations 2016-10-24 08:55:22 -04:00
Vlad
d08d5a3b6c - Updates unit tests 2016-10-24 08:55:22 -04:00
Vlad
8330bfc884 - Fixes "completed" status update of notification history
newsletters
- Fixes detection of post notification newsletters that do not contain any posts (i.e., blank ALC blocks)
- Updates unit test
2016-10-24 08:55:22 -04:00
Vlad
ef21a8cca7 - Enables post notification schedule update upon newsletter saving during
step 3
2016-10-24 08:55:22 -04:00
Vlad
e32c46a755 - Detaches posts_where action after posts are pulled from the database 2016-10-24 08:55:22 -04:00
Tautvidas Sipavičius
092f69538a Merge pull request #667 from mailpoet/sending_to_trashed_subscribers_fix
Prevents newsletters from being sent to trashed subscribers
2016-10-24 15:26:55 +03:00
Tautvidas Sipavičius
7a75367d75 Merge pull request #666 from mailpoet/export_filename_update
Increases export filename length and randomness
2016-10-24 13:36:36 +03:00
Tautvidas Sipavičius
0b2701ade2 Merge pull request #656 from mailpoet/security_issue_636
API Token
2016-10-24 13:26:44 +03:00
Vlad
1ac288d286 - Prevents newsletters from being sent to trashed subscribers
- Updates unit tests
- Addresses #629
2016-10-21 14:36:44 -04:00
Vlad
516bc73092 - Increases export filename length and randomness 2016-10-21 11:42:13 -04:00
Jonathan Labreuille
4088abef68 removed useless 'use' in unit test 2016-10-21 13:42:19 +02:00
Jonathan Labreuille
f6cefc3f5c wrong email address in unit test 2016-10-21 13:38:23 +02:00
Jonathan Labreuille
202e4b90e1 added unit test for API::checkPermissions 2016-10-21 13:36:41 +02:00
Jonathan Labreuille
ee89bf0722 refactored API class 2016-10-21 13:36:41 +02:00
Jonathan Labreuille
876d21300a fixed duplicated lines due to faulty rebase 2016-10-21 13:36:41 +02:00
Jonathan Labreuille
0ca5b7a79f API Security
- added APIAccess class to define access levels of API Endpoints (permissions)
- use "mailpoet_token" for all nonce (just as before)
- merged setupPublic/setupAdmin methods in API in order to avoid duplication
- check permission if access level is not all
- fixed ABSPATH check in some classes
2016-10-21 13:36:41 +02:00
Jonathan Labreuille
5d0ee43921 removed checkToken for admin ajax 2016-10-21 13:36:41 +02:00
Jonathan Labreuille
cc523a3c0b ability to specify action for generateToken() method 2016-10-21 13:36:41 +02:00
Jonathan Labreuille
2787998d32 Merge pull request #664 from mailpoet/editor_fixes
Editor fixes
2016-10-20 17:29:55 +02:00
Tautvidas Sipavičius
38f6c95059 Update newsletter saving to reflect code review comments
- Switch to using full segment objects when saving newsletters
- Fix stale comment in newsletter editor's Newsletter model
- Fix typo in newsletter editor tests
2016-10-20 17:52:05 +03:00
Tautvidas Sipavičius
cc03b631ff Allow newsletters.save endpoint to accept segments as list of objects 2016-10-20 16:08:41 +03:00
Tautvidas Sipavičius
a3c77fb685 Fix PHP to JS date format converter to handle escaped symbols 2016-10-20 15:19:04 +03:00
Tautvidas Sipavičius
3817e28960 Change newsletter not found error to a static one in editor 2016-10-20 13:38:07 +03:00
Tautvidas Sipavičius
c3a78b1ea3 Fix newsletter editor to only save properties it changes 2016-10-20 13:37:32 +03:00
mrcasual
42877236c8 Merge pull request #663 from mailpoet/wp_repo_files
Preparation for plugin repo
2016-10-19 09:08:54 -04:00
Tautvidas Sipavičius
6e87f3539c Update license.txt, readme.txt and link to plugin's repo page 2016-10-19 13:46:14 +03:00
Tautvidas Sipavičius
7704ea4b68 Bump up release version to 0.0.49 2016-10-19 13:23:00 +03:00
Tautvidas Sipavičius
12a3931b7b Merge pull request #662 from mailpoet/security_issue_634
PHP Object injection in front Router
2016-10-18 16:46:14 +03:00
Jonathan Labreuille
25a55dbb67 Merge pull request #661 from mailpoet/security_issue_633
Import SQL injection
2016-10-18 14:50:08 +02:00
Jonathan Labreuille
6758f60a81 Merge pull request #659 from mailpoet/import_data_sanitization
Sanitize import data
2016-10-18 12:35:52 +02:00
Vlad
5e9e53ec41 - Updates router to use json_encode() instead of serialize() for publicly modified data payload
- Updates unit tests
- Fixes #634
2016-10-17 22:39:36 -04:00
Vlad
1285252a8c - Adds unit tests 2016-10-17 20:27:58 -04:00
Vlad
98f95f72ad - Adds validation for import data, including column names (fixes #633)
- Prevents nonexistent custom fields from being associated with subscribers
2016-10-17 20:22:25 -04:00
Vlad
09ca788371 - Fixes subscriber count not being shown when new segment is created 2016-10-17 20:12:57 -04:00
Vlad
b48cc5a959 - Updates import UI to escape HTML text
- Allows mixing of escaped and unescaped HTML text
- Removes server-side text escaping
2016-10-17 11:01:54 -04:00
Jonathan Labreuille
812d138c4e Merge pull request #658 from mailpoet/import_and_mailer_host_restriction
Import and mailer host restriction
2016-10-17 16:19:42 +02:00
Jonathan Labreuille
07bc35d4cd Merge pull request #625 from mailpoet/unit_tests
Adds unit test for newsletter scheduler
2016-10-17 11:37:17 +02:00
Jonathan Labreuille
90b95a2c25 fixed 'newletter' typo and replaced integer weekdays by their carbon constant equivalent 2016-10-17 11:35:57 +02:00
Vlad
78c50c41e3 - Fixes unit test
- Updates code as per code review comments
2016-10-18 14:29:53 -04:00
mrcasual
7eee7def63 Merge pull request #657 from mailpoet/security_issue_635
Security issue #635
2016-10-16 16:42:11 -04:00
Vlad
9ba6e9806f - Adds data sanitization on the client and server side
- Closes #641
2016-10-16 13:02:49 -04:00
Vlad
8c28dc3d8a - Restricts Amazon SES region to a specific list of hosts
- Updates unit tests
- Closes #647
2016-10-16 12:19:47 -04:00
Vlad
9197e39fb4 - Restricts MailChimp API key to specific format
- Updates unit test
2016-10-16 11:57:56 -04:00
Jonathan Labreuille
37f59814e5 removed unused methods in Util/CSS -> fixes security issue #635 2016-10-13 10:34:36 +02:00
Vlad
e565a7a234 - Uses Codeception's native methods to verify expectations
- Updates next run date test conditions to use account for possible time
  difference
2016-09-26 12:35:00 -04:00
Vlad
e1c5f609ff - Adds unit test 2016-09-23 20:16:53 -04:00
83 changed files with 1366 additions and 483 deletions

View File

@@ -71,30 +71,43 @@ define('date',
convertFormat: function(format) {
var format_mappings = {
date: {
D: 'ddd',
l: 'dddd',
d: 'DD',
D: 'ddd',
j: 'D',
z: 'DDDD',
l: 'dddd',
N: 'E',
S: '',
M: 'MMM',
S: 'o',
w: 'e',
z: 'DDD',
W: 'W',
F: 'MMMM',
m: 'MM',
n: '',
t: '',
y: 'YY',
M: 'MMM',
n: 'M',
t: '', // no equivalent
L: '', // no equivalent
o: 'YYYY',
Y: 'YYYY',
H: 'HH',
h: 'hh',
g: 'h',
y: 'YY',
a: 'a',
A: 'A',
B: '', // no equivalent
g: 'h',
G: 'H',
h: 'hh',
H: 'HH',
i: 'mm',
s: 'ss',
T: 'z',
O: 'ZZ',
w: 'd',
W: 'WW'
u: 'SSS',
e: 'zz', // deprecated since version 1.6.0 of moment.js
I: '', // no equivalent
O: '', // no equivalent
P: '', // no equivalent
T: '', // no equivalent
Z: '', // no equivalent
c: '', // no equivalent
r: '', // no equivalent
U: 'X'
},
strftime: {
a: 'ddd',
@@ -127,20 +140,29 @@ define('date',
var replacements = format_mappings['date'];
var outputFormat = '';
var convertedFormat = [];
var escapeToken = false;
Object.keys(replacements).forEach(function(key) {
if (format.indexOf(key) !== -1) {
format = format.replace(key, '%'+key);
for (var index in format) {
var token = format[index];
if (escapeToken === true) {
convertedFormat.push('['+token+']');
escapeToken = false;
} else {
if (token === '\\') {
// Slash escapes the next symbol to be treated as literal
escapeToken = true;
continue;
} else if (replacements[token] !== undefined) {
convertedFormat.push(replacements[token]);
} else {
convertedFormat.push('['+token+']');
}
}
});
outputFormat = format;
Object.keys(replacements).forEach(function(key) {
if (outputFormat.indexOf('%'+key) !== -1) {
outputFormat = outputFormat.replace('%'+key, replacements[key]);
}
});
return outputFormat;
}
return convertedFormat.join('');
}
};
});

View File

@@ -122,9 +122,10 @@ function(
} else {
value = e.target.value;
}
var transformedValue = this.transformChangedValue(value);
this.props.onValueChange({
target: {
value: value,
value: transformedValue,
name: this.props.field.name
}
});
@@ -148,6 +149,16 @@ function(
}
return item.id;
},
// When it's impossible to represent the desired value in DOM,
// this function may be used to transform the placeholder value into
// desired value.
transformChangedValue: function(value) {
if(typeof this.props.field['transformChangedValue'] === 'function') {
return this.props.field.transformChangedValue.call(this, value);
} else {
return value;
}
},
render: function() {
const options = this.state.items.map((item, index) => {
let label = this.getLabel(item);

View File

@@ -11,15 +11,16 @@ define([
// Does not hold newsletter content nor newsletter styles, those are
// handled by other components.
Module.NewsletterModel = SuperModel.extend({
stale: ['body', 'created_at', 'deleted_at', 'updated_at'],
whitelisted: ['id', 'subject', 'preheader'],
initialize: function(options) {
this.on('change', function() {
App.getChannel().trigger('autoSave');
});
},
toJSON: function() {
// Remove stale attributes from resulting JSON object
return _.omit(SuperModel.prototype.toJSON.call(this), this.stale);
// Use only whitelisted properties to ensure properties editor
// doesn't control don't change.
return _.pick(SuperModel.prototype.toJSON.call(this), this.whitelisted);
},
});

View File

@@ -62,7 +62,7 @@ const _monthDayValues = _.object(
} else {
label = MailPoet.I18n.t('nth').replace("%$1d", day + 1);
}
return [day, label];
return [day + 1, label];
}
)
);

View File

@@ -1,11 +1,13 @@
define(
[
'mailpoet',
'newsletters/types/notification/scheduling.jsx'
'newsletters/types/notification/scheduling.jsx',
'underscore'
],
function(
MailPoet,
Scheduling
Scheduling,
_
) {
var settings = window.mailpoet_settings || {};
@@ -42,6 +44,14 @@ define(
getLabel: function(segment) {
return segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')';
},
transformChangedValue: function(segment_ids) {
var all_segments = this.state.items;
return _.map(segment_ids, function(id) {
return _.find(all_segments, function(segment) {
return segment.id === id;
});
});
},
validation: {
'data-parsley-required': true,
'data-parsley-required-message': MailPoet.I18n.t('noSegmentsSelectedError')

View File

@@ -348,6 +348,14 @@ define(
getLabel: function(segment) {
return segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')';
},
transformChangedValue: function(segment_ids) {
var all_segments = this.state.items;
return _.map(segment_ids, function(id) {
return _.find(all_segments, function(segment) {
return segment.id === id;
});
});
},
validation: {
'data-parsley-required': true,
'data-parsley-required-message': MailPoet.I18n.t('noSegmentsSelectedError')

View File

@@ -1,6 +1,5 @@
import _ from 'underscore'
import React from 'react'
import MailPoet from 'mailpoet'
import Select from 'form/fields/select.jsx'
import {
intervalValues,

View File

@@ -40,7 +40,7 @@ function(
// ajax request
MailPoet.Ajax.post({
url: MailPoetForm.ajax_url,
token: MailPoetForm.token,
token: data.token,
endpoint: 'subscribers',
action: 'subscribe',
data: data

View File

@@ -95,15 +95,18 @@ define(
});
};
// set confirmed subscribers export option to false
exportData.exportConfirmedOption = false;
renderSegmentsAndFields(subscriberFieldsContainerElement, subscriberFieldsSelect2);
renderSegmentsAndFields(segmentsContainerElement, segments);
subscriberFieldsContainerElement.select2('val', [
'status',
subscriberFieldsContainerElement.val([
'email',
'first_name',
'last_name'
]);
'last_name',
'status'
]).trigger("change");
exportConfirmedOptionElement.change(function () {
var selectedSegments = segmentsContainerElement.val();

View File

@@ -7,7 +7,6 @@ define(
'handlebars',
'papaparse',
'asyncqueue',
'xss',
'moment',
'select2'
],
@@ -19,7 +18,6 @@ define(
Handlebars,
Papa,
AsyncQueue,
xss,
Moment
) {
if (!jQuery('#mailpoet_subscribers_import').length) {
@@ -337,9 +335,9 @@ define(
complete: function (CSV) {
for (var rowCount in CSV.data) {
var rowData = CSV.data[rowCount].map(function (el) {
return filterXSS(el.trim());
}),
rowColumnCount = rowData.length;
return el.trim();
});
var rowColumnCount = rowData.length;
// set the number of row elements based on the first non-empty row
if (columnCount === null) {
columnCount = rowColumnCount;
@@ -582,7 +580,8 @@ define(
}).done(function(response) {
mailpoetSegments.push({
'id': response.data.id,
'name': response.data.name
'name': response.data.name,
'subscriberCount': 0
});
var selected_values = segmentSelectElement.val();
@@ -669,8 +668,15 @@ define(
return options.fn(displayedColumns);
});
// sanitize unsafe data
Handlebars.registerHelper('sanitize_data', function(data) {
return (data instanceof Handlebars.SafeString) ?
data :
new Handlebars.SafeString(Handlebars.Utils.escapeExpression(data));
});
// start array index from 1
Handlebars.registerHelper('show_real_index', function (index) {
Handlebars.registerHelper('calculate_index', function (index) {
var index = parseInt(index);
// display filler data (e.g., ellipsis) if we've reached the maximum number of rows and
// subscribers count is greater than the maximum number of rows we're displaying
@@ -865,7 +871,7 @@ define(
'<span class="mailpoet_data_match mailpoet_import_error" title="'
+ MailPoet.I18n.t('noDateFieldMatch') + '">'
+ MailPoet.I18n.t('emptyFirstRowDate')
+ '</span>';
+ '</span> ';
preventNextStep = true;
}
else {
@@ -879,7 +885,9 @@ define(
jQuery(matchedColumn.element).data('validation-rule', validationRule);
break;
}
if (validationRule === 'datetime') validationRule = Moment.ISO_8601;
if (validationRule === 'datetime') {
validationRule = Moment.ISO_8601;
}
}
}
jQuery.map(subscribersClone.subscribers, function (data, index) {
@@ -888,18 +896,22 @@ define(
var date = Moment(rowData, testedFormat, true);
// validate date
if (date.isValid()) {
data[matchedColumn.index] +=
'<span class="mailpoet_data_match" title="'
+ MailPoet.I18n.t('verifyDateMatch') + '">'
+ MailPoet.Date.format(date)
+ '</span>';
data[matchedColumn.index] = new Handlebars.SafeString(
Handlebars.Utils.escapeExpression(data[matchedColumn.index])
+ '<span class="mailpoet_data_match" title="'
+ MailPoet.I18n.t('verifyDateMatch') + '">'
+ MailPoet.Date.format(date)
+ '</span> '
);
}
else {
data[matchedColumn.index] +=
'<span class="mailpoet_data_match mailpoet_import_error" title="'
data[matchedColumn.index] = new Handlebars.SafeString(
Handlebars.Utils.escapeExpression(data[matchedColumn.index])
+ '<span class="mailpoet_data_match mailpoet_import_error" title="'
+ MailPoet.I18n.t('noDateFieldMatch') + '">'
+ MailPoet.I18n.t('dateMatchError')
+ '</span>';
+ (new Handlebars.SafeString(MailPoet.I18n.t('dateMatchError')))
+ '</span> '
);
preventNextStep = true;
};
});

View File

@@ -44,7 +44,7 @@ rm -rf $plugin_name/vendor/swiftmailer/swiftmailer/tests
rm -rf $plugin_name/vendor/cerdic/css-tidy/testing
# Copy release files.
cp LICENSE $plugin_name
cp license.txt $plugin_name
cp index.php $plugin_name
cp $plugin_name.php $plugin_name
cp readme.txt $plugin_name

View File

@@ -5,31 +5,40 @@ use \MailPoet\Util\Security;
if(!defined('ABSPATH')) exit;
class API {
private $_endpoint;
private $_method;
private $_token;
private $_endpoint_class;
private $_data = array();
function init() {
// security token
// Admin Security token
add_action(
'admin_head',
array($this, 'setToken')
);
// Admin API (Ajax only)
// ajax (logged in users)
add_action(
'wp_ajax_mailpoet',
array($this, 'setupAdmin')
array($this, 'setupAjax')
);
// Public API (Ajax)
// ajax (logged out users)
add_action(
'wp_ajax_nopriv_mailpoet',
array($this, 'setupPublic')
array($this, 'setupAjax')
);
}
function setupAdmin() {
function setupAjax() {
$this->getRequestData();
if($this->checkToken() === false) {
$error_response = new ErrorResponse(
array(
Error::UNAUTHORIZED => __('You need to specify a valid API token.', 'mailpoet')
Error::UNAUTHORIZED => __('Invalid request.', 'mailpoet')
),
array(),
Response::STATUS_UNAUTHORIZED
@@ -37,55 +46,84 @@ class API {
$error_response->send();
}
if($this->checkPermissions() === false) {
$error_response = new ErrorResponse(
array(
Error::FORBIDDEN => __('You do not have the required permissions.', 'mailpoet')
),
array(),
Response::STATUS_FORBIDDEN
);
$error_response->send();
}
$this->processRoute();
}
function setupPublic() {
if($this->checkToken() === false) {
function getRequestData() {
$this->_endpoint = isset($_POST['endpoint'])
? trim($_POST['endpoint'])
: null;
$this->_method = isset($_POST['method'])
? trim($_POST['method'])
: null;
$this->_token = isset($_POST['token'])
? trim($_POST['token'])
: null;
if(!$this->_endpoint || !$this->_method) {
// throw exception bad request
$error_response = new ErrorResponse(
array(
Error::UNAUTHORIZED => __('You need to specify a valid API token.', 'mailpoet')
Error::BAD_REQUEST => __('Invalid request.', 'mailpoet')
),
array(),
Response::STATUS_UNAUTHORIZED
Response::STATUS_BAD_REQUEST
);
$error_response->send();
}
} else {
$this->_endpoint_class = (
__NAMESPACE__."\\Endpoints\\".ucfirst($this->_endpoint)
);
$this->processRoute();
$this->_data = isset($_POST['data'])
? stripslashes_deep($_POST['data'])
: array();
// remove reserved keywords from data
if(is_array($this->_data) && !empty($this->_data)) {
// filter out reserved keywords from data
$reserved_keywords = array(
'token',
'endpoint',
'method',
'mailpoet_redirect'
);
$this->_data = array_diff_key(
$this->_data,
array_flip($reserved_keywords)
);
}
}
}
function processRoute() {
$class = ucfirst($_POST['endpoint']);
$endpoint = __NAMESPACE__ . "\\Endpoints\\" . $class;
$method = $_POST['method'];
$data = isset($_POST['data']) ? stripslashes_deep($_POST['data']) : array();
if(is_array($data) && !empty($data)) {
// filter out reserved keywords from data
$reserved_keywords = array(
'token',
'endpoint',
'method',
'mailpoet_redirect'
);
$data = array_diff_key($data, array_flip($reserved_keywords));
}
try {
$endpoint = new $endpoint();
$response = $endpoint->$method($data);
$endpoint = new $this->_endpoint_class();
// check the accessibility of the requested endpoint's action
// by default, an endpoint's action is considered "private"
$permissions = $endpoint->permissions;
if(
array_key_exists($this->_method, $permissions) === false
||
$permissions[$this->_method] !== Access::ALL
) {
if($this->checkPermissions() === false) {
$error_response = new ErrorResponse(
array(
Error::FORBIDDEN => __(
'You do not have the required permissions.',
'mailpoet'
)
),
array(),
Response::STATUS_FORBIDDEN
);
$error_response->send();
}
}
$response = $endpoint->{$this->_method}($this->_data);
$response->send();
} catch(\Exception $e) {
$error_response = new ErrorResponse(
@@ -95,22 +133,20 @@ class API {
}
}
function setToken() {
$global = '<script type="text/javascript">';
$global .= 'var mailpoet_token = "'.Security::generateToken().'";';
$global .= '</script>';
echo $global;
}
function checkPermissions() {
return current_user_can('manage_options');
}
function checkToken() {
return (
isset($_POST['token'])
&&
wp_verify_nonce($_POST['token'], 'mailpoet_token')
);
return wp_verify_nonce($this->_token, 'mailpoet_token');
}
function setToken() {
$global = '<script type="text/javascript">';
$global .= 'var mailpoet_token = "';
$global .= Security::generateToken();
$global .= '";';
$global .= '</script>';
echo $global;
}
}

12
lib/API/Access.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
namespace MailPoet\API;
if(!defined('ABSPATH')) exit;
final class Access {
const ALL = 'all';
private function __construct() {
}
}

View File

@@ -5,6 +5,8 @@ if(!defined('ABSPATH')) exit;
abstract class Endpoint {
public $permissions = array();
function successResponse(
$data = array(), $meta = array(), $status = Response::STATUS_OK
) {

View File

@@ -40,9 +40,9 @@ class Newsletters extends APIEndpoint {
}
function save($data = array()) {
$segment_ids = array();
$segments = array();
if(isset($data['segments'])) {
$segment_ids = $data['segments'];
$segments = $data['segments'];
unset($data['segments']);
}
@@ -58,13 +58,14 @@ class Newsletters extends APIEndpoint {
if(!empty($errors)) {
return $this->badRequest($errors);
} else {
if(!empty($segment_ids)) {
if(!empty($segments)) {
NewsletterSegment::where('newsletter_id', $newsletter->id)
->deleteMany();
foreach($segment_ids as $segment_id) {
foreach($segments as $segment) {
if(!is_array($segment)) continue;
$relation = NewsletterSegment::create();
$relation->segment_id = $segment_id;
$relation->segment_id = (int)$segment['id'];
$relation->newsletter_id = $newsletter->id;
$relation->save();
}
@@ -90,9 +91,14 @@ class Newsletters extends APIEndpoint {
}
}
return $this->successResponse(
Newsletter::findOne($newsletter->id)->asArray()
);
$newsletter = Newsletter::filter('filterWithOptions')->findOne($newsletter->id);
// if this is a post notification, process options and update its schedule
if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
Scheduler::processPostNotificationSchedule($newsletter);
}
return $this->successResponse($newsletter->asArray());
}
}

View File

@@ -2,6 +2,7 @@
namespace MailPoet\API\Endpoints;
use \MailPoet\API\Endpoint as APIEndpoint;
use \MailPoet\API\Error as APIError;
use \MailPoet\API\Access as APIAccess;
use MailPoet\Listing;
use MailPoet\Models\Subscriber;
@@ -15,6 +16,11 @@ use MailPoet\Models\StatisticsForms;
if(!defined('ABSPATH')) exit;
class Subscribers extends APIEndpoint {
public $permissions = array(
'subscribe' => APIAccess::ALL
);
function get($data = array()) {
$id = (isset($data['id']) ? (int)$data['id'] : false);
$subscriber = Subscriber::findOne($id);

View File

@@ -27,7 +27,6 @@ class Env {
static $db_password;
static $db_charset;
static $db_timezone_offset;
static $subscribers_limit = 2000;
static function init($file, $version) {
global $wpdb;
@@ -39,11 +38,11 @@ class Env {
self::$assets_path = self::$path . '/assets';
self::$assets_url = plugins_url('/assets', $file);
$wp_upload_dir = wp_upload_dir();
self::$temp_path = $wp_upload_dir['basedir'].'/'.self::$plugin_name;
self::$temp_path = $wp_upload_dir['basedir'] . '/' . self::$plugin_name;
if(!is_dir(self::$temp_path)) {
mkdir(self::$temp_path);
}
self::$temp_url = $wp_upload_dir['baseurl'].'/'.self::$plugin_name;
self::$temp_url = $wp_upload_dir['baseurl'] . '/' . self::$plugin_name;
self::$languages_path = self::$path . '/lang';
self::$lib_path = self::$path . '/lib';
self::$plugin_prefix = 'mailpoet_';

View File

@@ -4,6 +4,7 @@ namespace MailPoet\Config;
use MailPoet\Cron\CronTrigger;
use MailPoet\Router;
use MailPoet\API;
use MailPoet\Util\License\License as License;
use MailPoet\WP\Notice as WPNotice;
if(!defined('ABSPATH')) exit;
@@ -116,6 +117,7 @@ class Initializer {
function onInit() {
if(!$this->plugin_initialized) {
define('MAILPOET_INITIALIZED', false);
return;
}
@@ -126,6 +128,8 @@ class Initializer {
} catch(\Exception $e) {
$this->handleFailedInitialization($e);
}
define('MAILPOET_INITIALIZED', true);
}
function setupWidget() {
@@ -206,4 +210,4 @@ class Initializer {
function handleFailedInitialization($message) {
return WPNotice::displayError($message);
}
}
}

View File

@@ -14,6 +14,7 @@ use MailPoet\Settings\Hosts;
use MailPoet\Settings\Pages;
use MailPoet\Subscribers\ImportExport\ImportExportFactory;
use MailPoet\Listing;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\DateTime;
if(!defined('ABSPATH')) exit;
@@ -22,6 +23,8 @@ class Menu {
function __construct($renderer, $assets_url) {
$this->renderer = $renderer;
$this->assets_url = $assets_url;
$subscribers_feature = new SubscribersFeature();
$this->subscribers_over_limit = $subscribers_feature->check();
}
function init() {
@@ -34,16 +37,6 @@ class Menu {
);
}
function checkSubscribersLimit() {
$subscribers_count = Subscriber::getTotalSubscribers();
if($subscribers_count > Env::$subscribers_limit) {
echo $this->renderer->render('limit.html', array(
'limit' => Env::$subscribers_limit
));
exit;
}
}
function setup() {
$main_page_slug = 'mailpoet-newsletters';
@@ -59,8 +52,8 @@ class Menu {
$newsletters_page = add_submenu_page(
$main_page_slug,
$this->setPageTitle(__('Newsletters', 'mailpoet')),
__('Newsletters', 'mailpoet'),
$this->setPageTitle(__('Emails', 'mailpoet')),
__('Emails', 'mailpoet'),
'manage_options',
$main_page_slug,
array($this, 'newsletters')
@@ -252,7 +245,7 @@ class Menu {
}
function settings() {
$this->checkSubscribersLimit();
if ($this->subscribers_over_limit) return $this->displaySubscriberLimitExceededTemplate();
$settings = Setting::getAll();
$flags = $this->_getFlags();
@@ -326,7 +319,7 @@ class Menu {
}
function segments() {
$this->checkSubscribersLimit();
if ($this->subscribers_over_limit) return $this->displaySubscriberLimitExceededTemplate();
$data = array();
$data['items_per_page'] = $this->getLimitPerPage('segments');
@@ -334,7 +327,7 @@ class Menu {
}
function forms() {
$this->checkSubscribersLimit();
if ($this->subscribers_over_limit) return $this->displaySubscriberLimitExceededTemplate();
$data = array();
@@ -345,7 +338,7 @@ class Menu {
}
function newsletters() {
$this->checkSubscribersLimit();
if ($this->subscribers_over_limit) return $this->displaySubscriberLimitExceededTemplate();
global $wp_roles;
@@ -447,4 +440,11 @@ class Menu {
? (int)$listing_per_page
: Listing\Handler::DEFAULT_LIMIT_PER_PAGE;
}
}
function displaySubscriberLimitExceededTemplate() {
echo $this->renderer->render('limit.html', array(
'limit' => SubscribersFeature::SUBSCRIBERS_LIMIT
));
exit;
}
}

View File

@@ -69,8 +69,7 @@ class Widget {
'form' => $form_html,
'mailpoet_form' => array(
'ajax_url' => admin_url('admin-ajax.php', 'absolute'),
'is_rtl' => $is_rtl,
'token' => Security::generateToken()
'is_rtl' => $is_rtl
)
);
@@ -103,8 +102,7 @@ class Widget {
wp_localize_script('mailpoet_public', 'MailPoetForm', array(
'ajax_url' => admin_url('admin-ajax.php'),
'is_rtl' => (function_exists('is_rtl') ? (bool)is_rtl() : false),
'token' => Security::generateToken()
'is_rtl' => (function_exists('is_rtl') ? (bool)is_rtl() : false)
));
}

View File

@@ -47,6 +47,7 @@ class SendingQueue {
// abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
$found_subscribers = SubscriberModel::whereIn('id', $subscribers_to_process_ids)
->whereNull('deleted_at')
->findMany();
$found_subscribers_ids = array_map(function($subscriber) {
return $subscriber->id;

View File

@@ -8,7 +8,6 @@ use MailPoet\Models\Newsletter as NewsletterModel;
use MailPoet\Models\Setting;
use MailPoet\Newsletter\Links\Links as NewsletterLinks;
use MailPoet\Newsletter\Renderer\PostProcess\OpenTracking;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
@@ -45,7 +44,9 @@ class Newsletter {
}
// check if this is a post notification and if it contains posts
$newsletter_contains_posts = strpos($rendered_newsletter['html'], 'data-post-id');
if($newsletter->type === 'notification' && !$newsletter_contains_posts) {
if($newsletter->type === NewsletterModel::TYPE_NOTIFICATION_HISTORY &&
!$newsletter_contains_posts
) {
return false;
}
// extract and save newsletter posts
@@ -91,8 +92,10 @@ class Newsletter {
}
function markNewsletterAsSent($newsletter) {
// if it's a standard newsletter, update its status
if($newsletter->type === NewsletterModel::TYPE_STANDARD) {
// if it's a standard or notification history newsletter, update its status
if($newsletter->type === NewsletterModel::TYPE_STANDARD ||
$newsletter->type === NewsletterModel::TYPE_NOTIFICATION_HISTORY
) {
$newsletter->setStatus(NewsletterModel::STATUS_SENT);
}
}

View File

@@ -20,10 +20,10 @@ class Posts {
$newsletter->parent_id :
$newsletter->id;
foreach($matched_posts_ids as $post_id) {
$newletter_post = NewsletterPost::create();
$newletter_post->newsletter_id = $newsletter_id;
$newletter_post->post_id = $post_id;
$newletter_post->save();
$newsletter_post = NewsletterPost::create();
$newsletter_post->newsletter_id = $newsletter_id;
$newsletter_post->post_id = $post_id;
$newsletter_post->save();
}
return true;
}

View File

@@ -17,11 +17,19 @@ class AmazonSES {
public $reply_to;
public $date;
public $date_without_time;
const SES_REGIONS = array(
'US East (N. Virginia)' => 'us-east-1',
'US West (Oregon)' => 'us-west-2',
'EU (Ireland)' => 'eu-west-1'
);
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_region = (in_array($region, self::SES_REGIONS)) ? $region : false;
if(!$this->aws_region) {
throw new \Exception(__('Unsupported Amazon SES region.', 'mailpoet'));
}
$this->aws_endpoint = sprintf('email.%s.amazonaws.com', $this->aws_region);
$this->aws_signing_algorithm = 'AWS4-HMAC-SHA256';
$this->aws_service = 'ses';

View File

@@ -383,6 +383,7 @@ class Subscriber extends Model {
'subscribers.id = relation.subscriber_id',
'subscribers'
)
->whereNull('subscribers.deleted_at')
->where('subscribers.status', 'subscribed');
return $subscribers;
}

View File

@@ -15,12 +15,6 @@ class AutomatedLatestContent {
function __construct($newsletter_id = false, $newer_than_timestamp = false) {
$this->newsletter_id = $newsletter_id;
$this->newer_than_timestamp = $newer_than_timestamp;
$this->_attachSentPostsFilter();
}
function __destruct() {
$this->_detachSentPostsFilter();
}
function filterOutSentPosts($where) {
@@ -72,7 +66,10 @@ class AutomatedLatestContent {
);
}
return get_posts($parameters);
$this->_attachSentPostsFilter();
$posts = get_posts($parameters);
$this->_detachSentPostsFilter();
return $posts;
}
function transformPosts($args, $posts) {
@@ -129,4 +126,4 @@ class AutomatedLatestContent {
remove_action('posts_where', array($this, 'filterOutSentPosts'));
}
}
}
}

View File

@@ -46,10 +46,7 @@ class Router {
}
static function decodeRequestData($data) {
$data = base64_decode($data);
if(is_serialized($data)) {
$data = unserialize($data);
}
$data = json_decode(base64_decode($data), true);
if(!is_array($data)) {
$data = array();
}
@@ -57,7 +54,7 @@ class Router {
}
static function encodeRequestData($data) {
return rtrim(base64_encode(serialize($data)), '=');
return rtrim(base64_encode(json_encode($data)), '=');
}
static function buildRequest($endpoint, $action, $data) {

View File

@@ -1,6 +1,8 @@
<?php
namespace MailPoet\Settings;
use MailPoet\Mailer\Methods\AmazonSES;
class Hosts {
private static $_smtp = array(
'AmazonSES' => array(
@@ -12,11 +14,7 @@ class Hosts {
'access_key',
'secret_key'
),
'regions' => array(
'US East (N. Virginia)' => 'us-east-1',
'US West (Oregon)' => 'us-west-2',
'EU (Ireland)' => 'eu-west-1'
)
'regions' => AmazonSES::SES_REGIONS
),
'SendGrid' => array(
'name' => 'SendGrid',

View File

@@ -8,6 +8,7 @@ use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberSegment;
use MailPoet\Subscribers\ImportExport\ImportExportFactory;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security;
use MailPoet\Util\XLSXWriter;
class Export {
@@ -240,7 +241,7 @@ class Export {
function getExportFile($format) {
return sprintf(
$this->export_path . '/MailPoet_export_%s.%s',
substr(md5(time()), 0, 4),
Security::generateRandomString(15),
$format
);
}

View File

@@ -21,6 +21,7 @@ class Import {
public $updated_at;
public function __construct($data) {
$this->validateData($data);
$this->subscribers_data = $this->transformSubscribersData(
$data['subscribers'],
$data['columns']
@@ -41,6 +42,23 @@ class Import {
$this->updated_at = date('Y-m-d H:i:s', (int)$data['timestamp'] + 1);
}
function validateData($data) {
$required_data_fields = array(
'subscribers',
'columns',
'segments',
'timestamp',
'updateSubscribers'
);
// 1. data should contain all required fields
// 2. column names should only contain alphanumeric & underscore characters
if(count(array_intersect_key(array_flip($required_data_fields), $data)) !== count($required_data_fields) ||
preg_grep('/[^a-zA-Z0-9_]/', array_keys($data['columns']))
) {
throw new \Exception(__('Missing or invalid subscriber data.', 'mailpoet'));
}
}
function getSubscriberFieldsValidationRules($subscriber_fields) {
$validation_rules = array();
foreach($subscriber_fields as $column => $field) {
@@ -89,8 +107,8 @@ class Import {
$this->synchronizeWPUsers($wp_users);
}
}
} catch(\PDOException $e) {
throw new \Exception($e->getMessage());
} catch(\Exception $e) {
throw new \Exception(__('Unable to save imported subscribers.', 'mailpoet'));
}
$import_factory = new ImportExportFactory('import');
$segments = $import_factory->getSegments();
@@ -364,6 +382,11 @@ class Import {
$subscribers_data,
$subscriber_custom_fields
) {
// check if custom fields exist in the database
$subscriber_custom_fields = Helpers::flattenArray(
CustomField::whereIn('id', $subscriber_custom_fields)->select('id')->findArray()
);
if(!$subscriber_custom_fields) return;
$subscribers = array_map(
function($column) use ($db_subscribers, $subscribers_data) {
$count = range(0, count($subscribers_data[$column]) - 1);
@@ -406,4 +429,4 @@ class Import {
);
}
}
}
}

View File

@@ -4,8 +4,14 @@ namespace MailPoet\Subscribers\ImportExport\Import;
use MailPoet\Util\Helpers;
class MailChimp {
function __construct($APIKey, $lists = false) {
$this->api_key = $this->getAPIKey($APIKey);
public $api_key;
public $max_post_size;
public $data_center;
public $export_url;
const API_KEY_REGEX = '/[a-zA-Z0-9]{32}-[a-zA-Z0-9]{2,3}$/';
function __construct($api_key, $lists = false) {
$this->api_key = $this->getAPIKey($api_key);
$this->max_post_size = Helpers::getMaxPostSize('bytes');
$this->data_center = $this->getDataCenter($this->api_key);
$this->lists_url = 'https://%s.api.mailchimp.com/2.0/lists/list?apikey=%s';
@@ -108,15 +114,14 @@ class MailChimp {
);
}
function getDataCenter($APIKey) {
if(!preg_match('/-[a-zA-Z0-9]{3,}/', $APIKey)) return false;
// double parantheses: http://phpsadness.com/sad/51
$key_parts = explode('-', $APIKey);
return end($key_parts);
function getDataCenter($api_key) {
if(!$api_key) return false;
$api_key_parts = explode('-', $api_key);
return end($api_key_parts);
}
function getAPIKey($APIKey) {
return (preg_match('/[a-zA-Z0-9]{32}-[a-zA-Z0-9]{3,}/', $APIKey)) ? $APIKey : false;
function getAPIKey($api_key) {
return (preg_match(self::API_KEY_REGEX, $api_key)) ? $api_key : false;
}
function throwException($error) {
@@ -142,4 +147,4 @@ class MailChimp {
}
throw new \Exception($errorMessage);
}
}
}

View File

@@ -29,51 +29,13 @@ use csstidy;
*/
class CSS {
private $cssFiles = array();
private $parsed_css = array();
/*
* Retrieves a CSS stylesheet and caches it before returning it.
*/
public function getCSS($url)
{
if(!isset($cssFiles[$url]))
{
$cssFiles[$url] = file_get_contents($url);
}
return $cssFiles[$url];
}
/*
* Take a list of absolute URLs pointing to CSS stylesheets,
* retrieve the CSS, parse it, sort the rules by increasing order of specificity,
* cache the rules, return them.
*/
public function getCSSFromFiles($urls)
{
$key = implode('::', $urls);
if(!isset($this->parsed_css[$key]))
{
$texts = array();
foreach($urls as $url)
{
$texts[] = $this->getCSS($url);
}
$text = implode("\n\n", $texts);
$this->parsed_css[$key] = $text;
}
return $this->parsed_css[$key];
}
public static function splitMediaQueries($css)
{
public static function splitMediaQueries($css) {
$start = 0;
$queries = '';
while (($start = strpos($css, "@media", $start)) !== false)
{
while(($start = strpos($css, "@media", $start)) !== false) {
// stack to manage brackets
$s = array();
@@ -81,23 +43,18 @@ class CSS {
$i = strpos($css, "{", $start);
// if $i is false, then there is probably a css syntax error
if ($i !== false)
{
if($i !== false) {
// push bracket onto stack
array_push($s, $css[$i]);
// move past first bracket
$i++;
while (!empty($s))
{
while(!empty($s)) {
// if the character is an opening bracket, push it onto the stack, otherwise pop the stack
if ($css[$i] == "{")
{
if($css[$i] == "{") {
array_push($s, "{");
}
elseif ($css[$i] == "}")
{
} else if($css[$i] == "}") {
array_pop($s);
}
@@ -113,8 +70,7 @@ class CSS {
return array($css, $queries);
}
public function parseCSS($text)
{
public function parseCSS($text) {
$css = new csstidy();
$css->settings['compress_colors'] = false;
$css->parse($text);
@@ -122,41 +78,30 @@ class CSS {
$rules = array();
$position = 0;
foreach($css->css as $declarations)
{
foreach($declarations as $selectors => $properties)
{
foreach(explode(",", $selectors) as $selector)
{
foreach($css->css as $declarations) {
foreach($declarations as $selectors => $properties) {
foreach(explode(",", $selectors) as $selector) {
$rules[] = array(
'position' => $position,
'specificity' => self::calculateCSSSpecifity($selector),
'selector' => $selector,
'properties' => $properties
);
);
}
$position += 1;
}
}
usort($rules, function($a, $b){
if($a['specificity'] > $b['specificity'])
{
usort($rules, function($a, $b) {
if($a['specificity'] > $b['specificity']) {
return 1;
}
else if($a['specificity'] < $b['specificity'])
{
} else if($a['specificity'] < $b['specificity']) {
return -1;
}
else
{
if($a['position'] > $b['position'])
{
} else {
if($a['position'] > $b['position']) {
return 1;
}
else
{
} else {
return -1;
}
}
@@ -176,8 +121,7 @@ class CSS {
* @license BSD License
*/
public static function calculateCSSSpecifity($selector)
{
public static function calculateCSSSpecifity($selector) {
// cleanup selector
$selector = str_replace(array('>', '+'), array(' > ', ' + '), $selector);
@@ -207,16 +151,15 @@ class CSS {
* Turns a CSS style string (like: "border: 1px solid black; color:red")
* into an array of properties (like: array("border" => "1px solid black", "color" => "red"))
*/
public static function styleToArray($str)
{
public static function styleToArray($str) {
$array = array();
if(trim($str) === '')return $array;
if(trim($str) === '') return $array;
foreach(explode(';', $str) as $kv)
{
if ($kv === '')
foreach(explode(';', $str) as $kv) {
if($kv === '') {
continue;
}
$key_value = explode(':', $kv);
$array[trim($key_value[0])] = trim($key_value[1]);
@@ -229,52 +172,14 @@ class CSS {
* Reverses what styleToArray does, see above.
* array("border" => "1px solid black", "color" => "red") yields "border: 1px solid black; color:red"
*/
public static function arrayToStyle($array)
{
public static function arrayToStyle($array) {
$parts = array();
foreach($array as $k => $v)
{
foreach($array as $k => $v) {
$parts[] = "$k:$v";
}
return implode(';', $parts);
}
/*
* Get an absolute URL from an URL ($relative_url, but relative or not actually!)
* that is found on the page with url $page_url.
* Determine it as a browser would do. For instance if "<a href='/bob/hello.html'>hi</a>"
* (here '/bob/hello.html' is the $relative_url)
* is found on a page at $page_url := "http://example.com/stuff/index.html"
* then the function returns "http://example.com/bob/hello.html"
* because that's where you'd go to if you clicked on the link in your browser.
* This is used to find where to download the CSS files from when inlining.
*/
public static function absolutify($page_url, $relative_url)
{
$parsed_url = parse_url($page_url);
$absolute_url = '';
$parsed_relative_url = parse_url($relative_url);
// If $relative_url has a host it is actually absolute, return it.
if(isset($parsed_relative_url['host']))
{
$absolute_url = $relative_url;
}
// If $relative_url begins with / then it is a path relative to the $page_url's host
else if(preg_match('/^\//', $parsed_relative_url['path']))
{
$absolute_url = $parsed_url['scheme'].'://'.$parsed_url['host'].$parsed_relative_url['path'];
}
// No leading slash: append the path of $relative_url to the 'folder' path of $page_url
else
{
$absolute_url = $parsed_url['scheme'].'://'.$parsed_url['host'].dirname($parsed_url['path']).'/'.$parsed_relative_url['path'];
}
return $absolute_url;
}
/*
* The core of the algorithm, takes a URL and returns the HTML found there with the CSS inlined.
* If you pass $contents then the original HTML is not downloaded and $contents is used instead.
@@ -283,38 +188,24 @@ class CSS {
function inlineCSS($url, $contents=null)
{
// Download the HTML if it was not provided
if($contents === null)
{
if($contents === null) {
$html = HtmlDomParser::file_get_html($url, false, null, -1, -1, true, true, DEFAULT_TARGET_CHARSET, false, DEFAULT_BR_TEXT, DEFAULT_SPAN_TEXT);
}
// Else use the data provided!
else
{
} else {
// use the data provided!
$html = HtmlDomParser::str_get_html($contents, true, true, DEFAULT_TARGET_CHARSET, false, DEFAULT_BR_TEXT, DEFAULT_SPAN_TEXT);
}
if(!is_object($html))
{
if(!is_object($html)) {
return false;
}
$css_urls = array();
// Find all stylesheets and determine their absolute URLs to retrieve them
foreach($html->find('link[rel="stylesheet"]') as $style)
{
$css_urls[] = self::absolutify($url, $style->href);
$style->outertext = '';
}
$css_blocks = '';
// Find all <style> blocks and cut styles from them (leaving media queries)
foreach($html->find('style') as $style)
{
foreach($html->find('style') as $style) {
list($_css_to_parse, $_css_to_keep) = self::splitMediaQueries($style->innertext());
$css_blocks .= $_css_to_parse;
if (!empty($_css_to_keep)) {
if(!empty($_css_to_keep)) {
$style->innertext = $_css_to_keep;
} else {
$style->outertext = '';
@@ -322,10 +213,7 @@ class CSS {
}
$raw_css = '';
if (!empty($css_urls)) {
$raw_css .= $this->getCSSFromFiles($css_urls);
}
if (!empty($css_blocks)) {
if(!empty($css_blocks)) {
$raw_css .= $css_blocks;
}
@@ -336,10 +224,8 @@ class CSS {
// We loop over each rule by increasing order of specificity, find the nodes matching the selector
// and apply the CSS properties
foreach ($rules as $rule)
{
foreach($html->find($rule['selector']) as $node)
{
foreach ($rules as $rule) {
foreach($html->find($rule['selector']) as $node) {
// I'm leaving this for debug purposes, it has proved useful.
/*
if($node->already_styled === 'yes')
@@ -357,11 +243,11 @@ class CSS {
}//*/
// Unserialize the style array, merge the rule's CSS into it...
$nodeStyles = self::styleToArray( $node->style );
$style = array_merge( $nodeStyles, $rule[ 'properties' ] );
$nodeStyles = self::styleToArray($node->style);
$style = array_merge($nodeStyles, $rule['properties']);
// !important node styles should take precedence over other styles
$style = array_merge( $style, preg_grep( "/important/i", $nodeStyles ) );
$style = array_merge($style, preg_grep("/important/i", $nodeStyles));
// And put the CSS back as a string!
$node->style = self::arrayToStyle($style);
@@ -378,14 +264,10 @@ class CSS {
// Now a tricky part: do a second pass with only stuff marked !important
// because !important properties do not care about specificity, except when fighting
// against another !important property
foreach ($rules as $rule)
{
foreach($rule['properties'] as $key => $value)
{
if(strpos($value, '!important') !== false)
{
foreach($html->find($rule['selector']) as $node)
{
foreach ($rules as $rule) {
foreach($rule['properties'] as $key => $value) {
if(strpos($value, '!important') !== false) {
foreach($html->find($rule['selector']) as $node) {
$style = self::styleToArray($node->style);
$style[$key] = $value;
$node->style = self::arrayToStyle($style);

View File

@@ -0,0 +1,19 @@
<?php
namespace MailPoet\Util\License\Features;
use MailPoet\Models\Subscriber as SubscriberModel;
use MailPoet\Util\License\License;
class Subscribers {
public $license;
const SUBSCRIBERS_LIMIT = 2000;
function __construct($license = false) {
$this->license = ($license) ? $license : License::getLicense();
}
function check($subscribers_limit = self::SUBSCRIBERS_LIMIT) {
if($this->license) return false;
return SubscriberModel::getTotalSubscribers() > $subscribers_limit;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace MailPoet\Util\License;
class License {
static function getLicense($license = false) {
if(!$license) {
$license = defined('MAILPOET_PREMIUM_LICENSE') ?
MAILPOET_PREMIUM_LICENSE :
false;
}
return $license;
}
}

View File

@@ -5,8 +5,8 @@ if(!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-includes/pluggable.php');
class Security {
static function generateToken() {
return wp_create_nonce('mailpoet_token');
static function generateToken($action = 'mailpoet_token') {
return wp_create_nonce($action);
}
static function generateRandomString($length = 5) {

View File

@@ -1,3 +1,42 @@
WordPress - Web publishing software
Copyright 2011-2016 by the contributors
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
This program incorporates work covered by the following copyright and
permission notices:
b2 is (c) 2001, 2002 Michel Valdrighi - m@tidakada.com -
http://tidakada.com
Wherever third party code has been used, credit has been given in the code's
comments.
b2 is released under the GPL
and
WordPress - Web publishing software
Copyright 2003-2010 by the contributors
WordPress is released under the GPL
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
@@ -336,4 +375,11 @@ This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
Public License instead of this License.
WRITTEN OFFER
The source code for any program binaries or compressed scripts that are
included with WordPress can be freely obtained at the following URL:
https://wordpress.org/download/source/

View File

@@ -4,7 +4,7 @@ if(!defined('ABSPATH')) exit;
use \MailPoet\Config\Initializer;
/*
* Plugin Name: MailPoet
* Version: 0.0.48
* Version: 3.0.0-beta.1
* Plugin URI: http://www.mailpoet.com
* Description: MailPoet Newsletters.
* Author: MailPoet
@@ -22,7 +22,7 @@ use \MailPoet\Config\Initializer;
require 'vendor/autoload.php';
define('MAILPOET_VERSION', '0.0.48');
define('MAILPOET_VERSION', '3.0.0-beta.1');
$initializer = new Initializer(array(
'file' => __FILE__,

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,41 +1,89 @@
=== MailPoet ===
Contributors: mailpoet
Donate link: http://mailpoet.com
Tags: wordpress, plugin
Requires at least: 4.0
Tested up to: 4.0
Stable tag: 1.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
MailPoet newsletters.
== Description ==
Long description.
== Installation ==
Installation instructions.
== Screenshots ==
Screenshots.
== Frequently Asked Questions ==
= Question? =
Answer.
== Changelog ==
= 1.0 =
* 2020-01-01
* Initial release
== Upgrade Notice ==
= 1.0 =
* 2020-01-01
* Initial release
=== MailPoet 3 - Beta Version ===
Contributors: mailpoet
Tags: newsletter, email, welcome email, post notification, autoresponder, mailchimp, signup, smtp
Requires at least: 4.6
Tested up to: 4.6.1
Stable tag: 3.0.0-beta.1
Create and send beautiful emails and newsletters from WordPress.
== Description ==
Try the new MailPoet! This is a beta version of our completely new email newsletter plugin.(https://wordpress.org/plugins/wysija-newsletters/).
= What's new? =
* New email designer
* Responsive templates
* Send with MailPoet's sending service
* Fast user interface
* Easier initial configuration
[Try the demo.](http://demo3.mailpoet.com/launch/)
= Check out this 2 minute video. =
[vimeo https://vimeo.com/183339372]
= Use at your own risk! =
Use [the current stable MailPoet](https://wordpress.org/plugins/wysija-newsletters/) instead of this version if you are not a power user.
* This beta version is for testing purposes only!
* Not RTL compatible
* We expect bug reports from you!
* Multisite not supported
* No migration script from MailPoet 2.X to this version
* Weekly releases
= Premium version =
Not available yet. Limited stats in free version.
= Translations in your language =
We accept translations in the repository.
== Installation ==
There are 3 ways to install this plugin:
= 1. The super easy way =
1. In your Admin, go to menu Plugins > Add
1. Search for `mailpoet`
1. Click to install
1. Activate the plugin
1. A new menu `mailpoet` will appear in your Admin
= 2. The easy way =
1. Download the plugin (.zip file) on the right column of this page
1. In your Admin, go to menu Plugins > Add
1. Select the tab "Upload"
1. Upload the .zip file you just downloaded
1. Activate the plugin
1. A new menu `MailPoet` will appear in your Admin
= 3. The old and reliable way (FTP) =
1. Upload `mailpoet` folder to the `/wp-content/plugins/` directory
1. Activate the plugin through the 'Plugins' menu in WordPress
1. A new menu `MailPoet` will appear in your Admin
== Frequently Asked Questions ==
= Need help? =
Our [support site](https://docs.mailpoet.com/) has plenty of articles. You can write to us on the forums too.
== Screenshots ==
1. Sample newsletters.
2. The drag & drop editor.
3. Subscriber management.
4. Sending method configuration in Settings.
5. Importing subscribers with a CSV or from MailChimp.
== Changelog ==
= 3.0.0-beta.1 - 2016-10 =
* Initial public beta release.

View File

@@ -19,7 +19,7 @@ define([
data2: 'data2Value',
},
},
someField: 'someValue'
subject: 'my test subject'
});
});
@@ -28,13 +28,32 @@ define([
global.stubChannel(EditorApplication, {
trigger: mock,
});
model.set('someField', 'anotherValue');
model.set('subject', 'another test subject');
mock.verify();
});
it('does not include styles and content attributes in its JSON', function() {
it('does not include styles and content properties in its JSON', function() {
var json = model.toJSON();
expect(json).to.deep.equal({someField: 'someValue'});
expect(json).to.deep.equal({subject: 'my test subject'});
});
describe('toJSON()', function() {
it('will only contain properties modifiable by the editor', function() {
var model = new (ContentComponent.NewsletterModel)({
id: 19,
subject: 'some subject',
preheader: 'some preheader',
segments: [1, 2, 3],
modified_at: '2000-01-01 12:01:02',
someField: 'someValue'
});
var json = model.toJSON();
expect(json.id).to.equal(19);
expect(json.subject).to.equal('some subject');
expect(json.preheader).to.equal('some preheader');
expect(json).to.not.include.keys('segments', 'modified_at', 'someField');
})
});
});

View File

@@ -0,0 +1,38 @@
<?php
use \MailPoet\API\API;
// required to be able to use wp_delete_user()
require_once(ABSPATH.'wp-admin/includes/user.php');
class APITest extends MailPoetTest {
function _before() {
// create WP user
$this->wp_user_id = null;
$wp_user_id = wp_create_user('WP User', 'pass', 'wp_user@mailpoet.com');
if(is_wp_error($wp_user_id)) {
// user already exists
$this->wp_user_id = email_exists('wp_user@mailpoet.com');
} else {
$this->wp_user_id = $wp_user_id;
}
$this->api = new API();
}
function testItChecksPermissions() {
// logged out user
expect($this->api->checkPermissions())->false();
// give administrator role to wp user
$wp_user = get_user_by('id', $this->wp_user_id);
$wp_user->add_role('administrator');
wp_set_current_user($wp_user->ID, $wp_user->user_login);
// administrator should have permission
expect($this->api->checkPermissions())->true();
}
function _after() {
wp_delete_user($this->wp_user_id);
}
}

View File

@@ -1,11 +1,11 @@
<?php
use \MailPoet\API\Response as APIResponse;
use \MailPoet\API\Error as APIError;
use \MailPoet\API\Endpoints\Newsletters;
use \MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterOptionField;
use \MailPoet\Models\NewsletterSegment;
use \MailPoet\Models\NewsletterTemplate;
use \MailPoet\Models\Segment;
use MailPoet\Newsletter\Scheduler\Scheduler;
class NewslettersTest extends MailPoetTest {
function _before() {
@@ -44,17 +44,26 @@ class NewslettersTest extends MailPoetTest {
}
function testItCanSaveANewNewsletter() {
$newsletter_option_field = NewsletterOptionField::create();
$newsletter_option_field->name = 'some_option';
$newsletter_option_field->newsletter_type = Newsletter::TYPE_STANDARD;
$newsletter_option_field->save();
$valid_data = array(
'subject' => 'My First Newsletter',
'type' => Newsletter::TYPE_STANDARD
'type' => Newsletter::TYPE_STANDARD,
'options' => array(
$newsletter_option_field->name => 'some_option_value',
)
);
$router = new Newsletters();
$response = $router->save($valid_data);
$saved_newsletter = Newsletter::filter('filterWithOptions')->findOne($response->data['id']);
expect($response->status)->equals(APIResponse::STATUS_OK);
expect($response->data)->equals(
Newsletter::findOne($response->data['id'])->asArray()
);
expect($response->data)->equals($saved_newsletter->asArray());
// newsletter option should be saved
expect($saved_newsletter->some_option)->equals('some_option_value');
$invalid_data = array(
'subject' => 'Missing newsletter type'
@@ -73,13 +82,75 @@ class NewslettersTest extends MailPoetTest {
);
$response = $router->save($newsletter_data);
$updated_newsletter = Newsletter::findOne($this->newsletter->id);
expect($response->status)->equals(APIResponse::STATUS_OK);
expect($response->data)->equals(
Newsletter::findOne($this->newsletter->id)->asArray()
expect($response->data)->equals($updated_newsletter->asArray());
expect($updated_newsletter->subject)->equals('My Updated Newsletter');
}
function testItCanUpdatePostNotificationScheduleUponSave() {
$newsletter_options = array(
'intervalType',
'timeOfDay',
'weekDay',
'monthDay',
'nthWeekDay',
'schedule'
);
foreach($newsletter_options as $option) {
$newsletter_option_field = NewsletterOptionField::create();
$newsletter_option_field->name = $option;
$newsletter_option_field->newsletter_type = Newsletter::TYPE_NOTIFICATION;
$newsletter_option_field->save();
}
$router = new Newsletters();
$newsletter_data = array(
'id' => $this->newsletter->id,
'type' => Newsletter::TYPE_NOTIFICATION,
'subject' => 'Newsletter',
'options' => array(
'intervalType' => Scheduler::INTERVAL_WEEKLY,
'timeOfDay' => '50400',
'weekDay' => '1',
'monthDay' => '0',
'nthWeekDay' => '1',
'schedule' => '0 14 * * 1'
)
);
$response = $router->save($newsletter_data);
$saved_newsletter = Newsletter::filter('filterWithOptions')
->findOne($response->data['id']);
expect($response->status)->equals(APIResponse::STATUS_OK);
expect($response->data)->equals($saved_newsletter->asArray());
// schedule should be recalculated when options change
$newsletter_data['options']['intervalType'] = Scheduler::INTERVAL_IMMEDIATELY;
$response = $router->save($newsletter_data);
$saved_newsletter = Newsletter::filter('filterWithOptions')->findOne($response->data['id']);
expect($response->status)->equals(APIResponse::STATUS_OK);
expect($saved_newsletter->schedule)->equals('* * * * *');
}
function testItCanModifySegmentsOfExistingNewsletter() {
$segment_1 = Segment::createOrUpdate(array('name' => 'Segment 1'));
$fake_segment_id = 1;
$router = new Newsletters();
$newsletter_data = array(
'id' => $this->newsletter->id,
'subject' => 'My Updated Newsletter',
'segments' => array($segment_1->asArray(), $fake_segment_id)
);
$updated_newsletter = Newsletter::findOne($this->newsletter->id);
expect($updated_newsletter->subject)->equals('My Updated Newsletter');
$response = $router->save($newsletter_data);
expect($response->status)->equals(APIResponse::STATUS_OK);
$updated_newsletter =
Newsletter::findOne($this->newsletter->id)->withSegments();
expect(count($updated_newsletter->segments))->equals(1);
expect($updated_newsletter->segments[0]['name'])->equals('Segment 1');
}
function testItCanSetANewsletterStatus() {
@@ -377,6 +448,7 @@ class NewslettersTest extends MailPoetTest {
function _after() {
Newsletter::deleteMany();
NewsletterSegment::deleteMany();
NewsletterOptionField::deleteMany();
Segment::deleteMany();
}
}

View File

@@ -1,5 +1,6 @@
<?php
use Carbon\Carbon;
use Codeception\Util\Fixtures;
use Codeception\Util\Stub;
use MailPoet\API\Endpoints\Cron;
@@ -298,6 +299,29 @@ class SendingQueueTest extends MailPoetTest {
expect($statistics)->false();
}
function testItDoesNotSendToTrashedSubscribers() {
$sending_queue_worker = $this->sending_queue_worker;
$sending_queue_worker->mailer_task = Stub::make(
new MailerTask(),
array('send' => function($newsletter, $subscriber) { return true; })
);
// newsletter is sent to existing subscriber
$sending_queue_worker->process();
$updated_queue = SendingQueue::findOne($this->queue->id);
expect((int)$updated_queue->count_total)->equals(1);
// newsletter is not sent to trashed subscriber
$this->_after();
$this->_before();
$subscriber = $this->subscriber;
$subscriber->deleted_at = Carbon::now();
$subscriber->save();
$sending_queue_worker->process();
$updated_queue = SendingQueue::findOne($this->queue->id);
expect((int)$updated_queue->count_total)->equals(0);
}
function _after() {
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);

View File

@@ -76,7 +76,8 @@ class NewsletterTaskTest extends MailPoetTest {
function testReturnsFalseWhenNewsletterIsANotificationWithoutPosts() {
$newsletter = $this->newsletter;
$newsletter->type = Newsletter::TYPE_NOTIFICATION;
$newsletter->type = Newsletter::TYPE_NOTIFICATION_HISTORY;
// replace post id data tag with something else
$newsletter->body = str_replace('data-post-id', 'id', $newsletter->body);
$newsletter->save();
@@ -92,17 +93,27 @@ class NewsletterTaskTest extends MailPoetTest {
expect($newsletter_post->post_id)->equals('10');
}
function testItUpdatesStatusToSentOnlyForStandardNewsletters() {
// newsletter type is 'standard'
function testItUpdatesStatusToSentOnlyForStandardAndPostNotificationNewsletters() {
$newsletter = $this->newsletter;
expect($newsletter->type)->equals(Newsletter::TYPE_STANDARD);
expect($newsletter->status)->notEquals(Newsletter::STATUS_SENT);
// newsletter type is 'standard'
$newsletter->type = Newsletter::TYPE_STANDARD;
$newsletter->status = 'not_sent';
$newsletter->save();
$this->newsletter_task->markNewsletterAsSent($newsletter);
$updated_newsletter = Newsletter::findOne($newsletter->id);
expect($updated_newsletter->status)->equals(Newsletter::STATUS_SENT);
// newsletter type is NOT 'standard'
$newsletter->type = Newsletter::TYPE_NOTIFICATION;
// newsletter type is 'notification history'
$newsletter->type = Newsletter::TYPE_NOTIFICATION_HISTORY;
$newsletter->status = 'not_sent';
$newsletter->save();
$this->newsletter_task->markNewsletterAsSent($newsletter);
$updated_newsletter = Newsletter::findOne($newsletter->id);
expect($updated_newsletter->status)->equals(Newsletter::STATUS_SENT);
// all other newsletter types
$newsletter->type = Newsletter::TYPE_WELCOME;
$newsletter->status = 'not_sent';
$newsletter->save();
$this->newsletter_task->markNewsletterAsSent($newsletter);

View File

@@ -56,6 +56,21 @@ class AmazonSESTest extends MailPoetTest {
expect(preg_match('!^\d{8}$!', $this->mailer->date_without_time))->equals(1);
}
function testItChecksForValidRegion() {
try {
$mailer = new AmazonSES(
'random_region',
$this->settings['access_key'],
$this->settings['secret_key'],
$this->sender,
$this->reply_to
);
$this->fail('Unsupported region exception was not thrown');
} catch(\Exception $e) {
expect($e->getMessage())->equals('Unsupported Amazon SES region.');
}
}
function testItCanGenerateBody() {
$body = $this->mailer->getBody($this->newsletter, $this->subscriber);
expect($body['Action'])->equals('SendEmail');

View File

@@ -1,5 +1,6 @@
<?php
use Carbon\Carbon;
use MailPoet\Models\CustomField;
use MailPoet\Models\Segment;
use MailPoet\Models\Subscriber;
@@ -369,7 +370,7 @@ class SubscriberTest extends MailPoetTest {
->equals('non_default_value');
}
function testItCanGetOnlySubscribedSubscribersInSegments() {
function testItCanGetOnlySubscribedAndNonTrashedSubscribersInSegments() {
$subscriber_1 = Subscriber::createOrUpdate(array(
'first_name' => 'Adam',
'last_name' => 'Smith',
@@ -384,13 +385,20 @@ class SubscriberTest extends MailPoetTest {
'status' => Subscriber::STATUS_SUBSCRIBED
));
$subscriber_3 = Subscriber::createOrUpdate(array(
'first_name' => 'Bob',
'last_name' => 'Smith',
'email' => 'bob@smith.com',
'status' => Subscriber::STATUS_SUBSCRIBED,
'deleted_at' => Carbon::now()
));
$segment = Segment::createOrUpdate(array(
'name' => 'Only Subscribed Subscribers Segment'
));
//Subscriber::createMultiple($columns, $values);
$result = SubscriberSegment::subscribeManyToSegments(
array($subscriber_1->id, $subscriber_2->id),
array($subscriber_1->id, $subscriber_2->id, $subscriber_3->id),
array($segment->id)
);
expect($result)->true();

View File

@@ -66,10 +66,10 @@ class LinksTest extends MailPoetTest {
expect($result)
->regExp('/<img src="http.*?' . Router::NAME . '&endpoint=track&action=open&data=.*?>/');
// data was base64encoded, serialized and contains an array of variables
// data was properly encoded
preg_match_all('/data=(?P<data>.*?)"/', $result, $result);
foreach($result['data'] as $data) {
$data = unserialize(base64_decode($data));
$data = Router::decodeRequestData($data);
expect($data['subscriber_id'])->equals($subscriber->id);
expect($data['queue_id'])->equals($queue->id);
expect(isset($data['subscriber_token']))->true();
@@ -85,7 +85,7 @@ class LinksTest extends MailPoetTest {
);
Links::save(
$links,
$newletter_id = 1,
$newsletter_id = 1,
$queue_id = 1
);

View File

@@ -0,0 +1,455 @@
<?php
use Carbon\Carbon;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterOption;
use MailPoet\Models\NewsletterOptionField;
use MailPoet\Models\NewsletterPost;
use MailPoet\Models\SendingQueue;
use MailPoet\Newsletter\Scheduler\Scheduler;
class NewsletterSchedulerTest extends MailPoetTest {
function testItSetsConstants() {
expect(Scheduler::SECONDS_IN_HOUR)->notEmpty();
expect(Scheduler::LAST_WEEKDAY_FORMAT)->notEmpty();
expect(Scheduler::WORDPRESS_ALL_ROLES)->notEmpty();
expect(Scheduler::INTERVAL_IMMEDIATELY)->notEmpty();
expect(Scheduler::INTERVAL_IMMEDIATE)->notEmpty();
expect(Scheduler::INTERVAL_DAILY)->notEmpty();
expect(Scheduler::INTERVAL_WEEKLY)->notEmpty();
expect(Scheduler::INTERVAL_MONTHLY)->notEmpty();
expect(Scheduler::INTERVAL_NTHWEEKDAY)->notEmpty();
}
function testItGetsActiveNewslettersFilteredByType() {
$newsletter = $this->_createNewsletter($type = Newsletter::TYPE_WELCOME);
// no newsletters wtih type "notification" should be found
expect(Scheduler::getNewsletters(Newsletter::TYPE_NOTIFICATION))->isEmpty();
// one newsletter with type "welcome" should be found
expect(Scheduler::getNewsletters(Newsletter::TYPE_WELCOME))->count(1);
}
function testItCanGetNextRunDate() {
// it accepts cron syntax and returns next run date
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
expect(Scheduler::getNextRunDate('* * * * *'))
->equals($current_time->addMinute()->format('Y-m-d H:i:00'));
}
function testItFormatsDatetimeString() {
expect(Scheduler::formatDatetimeString('April 20, 2016 4pm'))
->equals('2016-04-20 16:00:00');
}
function testItCreatesPostNotificationQueueRecord() {
$newsletter = $this->_createNewsletter();
$newsletter->schedule = '* 5 * * *';
// new queue record should be created
$queue = Scheduler::createPostNotificationQueue($newsletter);
expect(SendingQueue::findMany())->count(1);
expect($queue->newsletter_id)->equals($newsletter->id);
expect($queue->status)->equals(SendingQueue::STATUS_SCHEDULED);
expect($queue->scheduled_at)->equals(Scheduler::getNextRunDate('* 5 * * *'));
// duplicate queue record should not be created
Scheduler::createPostNotificationQueue($newsletter);
expect(SendingQueue::findMany())->count(1);
}
function testItCreatesWelcomeNotificationQueueRecord() {
$newsletter = (object)array(
'id' => 1,
'afterTimeNumber' => 2
);
// queue is scheduled delivery in 2 hours
$newsletter->afterTimeType = 'hours';
Scheduler::createWelcomeNotificationQueue($newsletter, $subscriber_id = 1);
$queue = SendingQueue::where('newsletter_id', 1)
->findOne();
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
expect($queue->id)->greaterOrEquals(1);
expect(Carbon::parse($queue->scheduled_at)->format('Y-m-d H:i'))
->equals($current_time->addHours(2)->format('Y-m-d H:i'));
$this->_after();
// queue is scheduled for delivery in 2 days
$newsletter->afterTimeType = 'days';
Scheduler::createWelcomeNotificationQueue($newsletter, $subscriber_id = 1);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$queue = SendingQueue::where('newsletter_id', 1)
->findOne();
expect($queue->id)->greaterOrEquals(1);
expect(Carbon::parse($queue->scheduled_at)->format('Y-m-d H:i'))
->equals($current_time->addDays(2)->format('Y-m-d H:i'));
$this->_after();
// queue is scheduled for delivery in 2 weeks
$newsletter->afterTimeType = 'weeks';
Scheduler::createWelcomeNotificationQueue($newsletter, $subscriber_id = 1);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$queue = SendingQueue::where('newsletter_id', 1)
->findOne();
expect($queue->id)->greaterOrEquals(1);
expect(Carbon::parse($queue->scheduled_at)->format('Y-m-d H:i'))
->equals($current_time->addWeeks(2)->format('Y-m-d H:i'));
$this->_after();
// queue is scheduled for immediate delivery
$newsletter->afterTimeType = null;
Scheduler::createWelcomeNotificationQueue($newsletter, $subscriber_id = 1);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$queue = SendingQueue::where('newsletter_id', 1)
->findOne();
expect($queue->id)->greaterOrEquals(1);
expect(Carbon::parse($queue->scheduled_at)->format('Y-m-d H:i'))
->equals($current_time->format('Y-m-d H:i'));
}
function tesIttDoesNotSchedulePostNotificationWhenNotificationWasAlreadySentForPost() {
$newsletter = $this->_createNewsletter();
$newsletter_post = NewsletterPost::create();
$newsletter_post->newsletter_id = $newsletter->id;
$newsletter_post->post_id = 10;
$newsletter_post->save();
// queue is not created when notification was already sent for the post
Scheduler::schedulePostNotification($post_id = 10);
$queue = SendingQueue::where('newsletter_id', $newsletter->id)
->findOne();
expect($queue)->false();
}
function testItSchedulesPostNotification() {
$newsletter = $this->_createNewsletter();
$this->_createNewsletterOptions(
$newsletter->id,
Newsletter::TYPE_NOTIFICATION,
array(
'schedule' => '* 5 * * *'
)
);
// queue is created and scheduled for delivery one day later at 5 a.m.
Scheduler::schedulePostNotification($post_id = 10);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$next_run_date = ($current_time->hour < 5) ?
$current_time :
$current_time->addDay();
$queue = SendingQueue::where('newsletter_id', $newsletter->id)
->findOne();
expect($queue->scheduled_at)->equals($next_run_date->format('Y-m-d 05:00:00'));
}
function testItDoesNotSchedulesSubscriberWelcomeNotificationWhenSubscriberIsNotInSegment() {
// do not schedule when subscriber is not in segment
$newsletter = $this->_createNewsletter(Newsletter::TYPE_WELCOME);
Scheduler::scheduleSubscriberWelcomeNotification(
$subscriber_id = 10,
$segments = array()
);
// queue is not created
$queue = SendingQueue::where('newsletter_id', $newsletter->id)
->findOne();
expect($queue)->false();
}
function testItSchedulesSubscriberWelcomeNotification() {
$newsletter = $this->_createNewsletter(Newsletter::TYPE_WELCOME);
$this->_createNewsletterOptions(
$newsletter->id,
Newsletter::TYPE_WELCOME,
array(
'event' => 'segment',
'segment' => 2,
'afterTimeType' => 'days',
'afterTimeNumber' => 1
)
);
// queue is created and scheduled for delivery one day later
Scheduler::scheduleSubscriberWelcomeNotification(
$subscriber_id = 10,
$segments = array(
3,
2,
1
)
);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$queue = SendingQueue::where('newsletter_id', $newsletter->id)
->findOne();
expect(Carbon::parse($queue->scheduled_at)->format('Y-m-d H:i'))
->equals($current_time->addDay()->format('Y-m-d H:i'));
}
function itDoesNotScheduleAnythingWhenNewsletterDoesNotExist() {
// post notification is not scheduled
expect(Scheduler::schedulePostNotification($post_id = 10))->false();
// subscriber welcome notification is not scheduled
$result = Scheduler::scheduleSubscriberWelcomeNotification(
$subscriber_id = 10,
$segments = array()
);
expect($result)->false();
// WP user welcome notification is not scheduled
$result = Scheduler::scheduleSubscriberWelcomeNotification(
$subscriber_id = 10,
$segments = array()
);
expect($result)->false();
}
function testItDoesNotScheduleWPUserWelcomeNotificationWhenRoleHasNotChanged() {
$newsletter = $this->_createNewsletter(Newsletter::TYPE_WELCOME);
$this->_createNewsletterOptions(
$newsletter->id,
Newsletter::TYPE_WELCOME,
array(
'event' => 'user',
'role' => 'editor',
'afterTimeType' => 'days',
'afterTimeNumber' => 1
)
);
Scheduler::scheduleWPUserWelcomeNotification(
$subscriber_id = 10,
$wp_user = (object)array('roles' => array('editor')),
$old_user_data = (object)array('roles' => array('editor'))
);
// queue is not created
$queue = SendingQueue::where('newsletter_id', $newsletter->id)
->findOne();
expect($queue)->false();
}
function testItDoesNotScheduleWPUserWelcomeNotificationWhenUserRoleDoesNotMatch() {
$newsletter = $this->_createNewsletter(Newsletter::TYPE_WELCOME);
$this->_createNewsletterOptions(
$newsletter->id,
Newsletter::TYPE_WELCOME,
array(
'event' => 'user',
'role' => 'editor',
'afterTimeType' => 'days',
'afterTimeNumber' => 1
)
);
Scheduler::scheduleWPUserWelcomeNotification(
$subscriber_id = 10,
$wp_user = (object)array('roles' => array('administrator'))
);
// queue is not created
$queue = SendingQueue::where('newsletter_id', $newsletter->id)
->findOne();
expect($queue)->false();
}
function testItSchedulesWPUserWelcomeNotificationWhenUserRolesMatches() {
$newsletter = $this->_createNewsletter(Newsletter::TYPE_WELCOME);
$this->_createNewsletterOptions(
$newsletter->id,
Newsletter::TYPE_WELCOME,
array(
'event' => 'user',
'role' => 'administrator',
'afterTimeType' => 'days',
'afterTimeNumber' => 1
)
);
Scheduler::scheduleWPUserWelcomeNotification(
$subscriber_id = 10,
$wp_user = (object)array('roles' => array('administrator'))
);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
// queue is created and scheduled for delivery one day later
$queue = SendingQueue::where('newsletter_id', $newsletter->id)
->findOne();
expect(Carbon::parse($queue->scheduled_at)->format('Y-m-d H:i'))
->equals($current_time->addDay()->format('Y-m-d H:i'));
}
function testItSchedulesWPUserWelcomeNotificationWhenUserHasAnyRole() {
$newsletter = $this->_createNewsletter(Newsletter::TYPE_WELCOME);
$this->_createNewsletterOptions(
$newsletter->id,
Newsletter::TYPE_WELCOME,
array(
'event' => 'user',
'role' => Scheduler::WORDPRESS_ALL_ROLES,
'afterTimeType' => 'days',
'afterTimeNumber' => 1
)
);
Scheduler::scheduleWPUserWelcomeNotification(
$subscriber_id = 10,
$wp_user = (object)array('roles' => array('administrator'))
);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
// queue is created and scheduled for delivery one day later
$queue = SendingQueue::where('newsletter_id', $newsletter->id)
->findOne();
expect(Carbon::parse($queue->scheduled_at)->format('Y-m-d H:i'))
->equals($current_time->addDay()->format('Y-m-d H:i'));
}
function testItProcessesPostNotificationSchedule() {
$newsletter_option_field = NewsletterOptionField::create();
$newsletter_option_field->name = 'schedule';
$newsletter_option_field->newsletter_type = Newsletter::TYPE_WELCOME;
$newsletter_option_field->save();
// daily notification is scheduled at 14:00
$newsletter = (object)array(
'id' => 1,
'intervalType' => Scheduler::INTERVAL_DAILY,
'monthDay' => null,
'nthWeekDay' => null,
'weekDay' => null,
'timeOfDay' => 50400 // 14:00
);
Scheduler::processPostNotificationSchedule($newsletter);
$newsletter_option = NewsletterOption::where('newsletter_id', $newsletter->id)
->where('option_field_id', $newsletter_option_field->id)
->findOne();
expect(Scheduler::getNextRunDate($newsletter_option->value))
->contains('14:00:00');
// weekly notification is scheduled every Tuesday at 14:00
$newsletter = (object)array(
'id' => 1,
'intervalType' => Scheduler::INTERVAL_WEEKLY,
'monthDay' => null,
'nthWeekDay' => null,
'weekDay' => Carbon::TUESDAY,
'timeOfDay' => 50400 // 14:00
);
Scheduler::processPostNotificationSchedule($newsletter);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$newsletter_option = NewsletterOption::where('newsletter_id', $newsletter->id)
->where('option_field_id', $newsletter_option_field->id)
->findOne();
$next_run_date = ($current_time->dayOfWeek === Carbon::TUESDAY && $current_time->hour < 14) ?
$current_time :
$current_time->next(Carbon::TUESDAY);
expect(Scheduler::getNextRunDate($newsletter_option->value))
->equals($next_run_date->format('Y-m-d 14:00:00'));
// monthly notification is scheduled every 20th day at 14:00
$newsletter = (object)array(
'id' => 1,
'intervalType' => Scheduler::INTERVAL_MONTHLY,
'monthDay' => 19, // 20th (count starts from 0)
'nthWeekDay' => null,
'weekDay' => null,
'timeOfDay' => 50400 // 14:00
);
Scheduler::processPostNotificationSchedule($newsletter);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$newsletter_option = NewsletterOption::where('newsletter_id', $newsletter->id)
->where('option_field_id', $newsletter_option_field->id)
->findOne();
expect(Scheduler::getNextRunDate($newsletter_option->value))
->contains('-19 14:00:00');
// monthly notification is scheduled every last Saturday at 14:00
$newsletter = (object)array(
'id' => 1,
'intervalType' => Scheduler::INTERVAL_NTHWEEKDAY,
'monthDay' => null,
'nthWeekDay' => 'L', // L = last
'weekDay' => Carbon::SATURDAY,
'timeOfDay' => 50400 // 14:00
);
Scheduler::processPostNotificationSchedule($newsletter);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$next_run_date = (
$current_time->day < $current_time->lastOfMonth(Carbon::SATURDAY)->day
) ? $current_time->lastOfMonth(Carbon::SATURDAY)
: $current_time->addMonth()->lastOfMonth(Carbon::SATURDAY);
$newsletter_option = NewsletterOption::where('newsletter_id', $newsletter->id)
->where('option_field_id', $newsletter_option_field->id)
->findOne();
expect(Scheduler::getNextRunDate($newsletter_option->value))
->equals($next_run_date->format('Y-m-d 14:00:00'));
// notification is scheduled immediately (next minute)
$newsletter = (object)array(
'id' => 1,
'intervalType' => Scheduler::INTERVAL_IMMEDIATELY,
'monthDay' => null,
'nthWeekDay' => null,
'weekDay' => null,
'timeOfDay' => null
);
Scheduler::processPostNotificationSchedule($newsletter);
$current_time = Carbon::createFromTimestamp(current_time('timestamp'));
$newsletter_option = NewsletterOption::where('newsletter_id', $newsletter->id)
->where('option_field_id', $newsletter_option_field->id)
->findOne();
expect(Scheduler::getNextRunDate($newsletter_option->value))
->equals($current_time->addMinute()->format('Y-m-d H:i:00'));
}
function _createQueue(
$newsletter_id,
$scheduled_at = null,
$status = SendingQueue::STATUS_SCHEDULED
) {
$queue = SendingQueue::create();
$queue->status = $status;
$queue->newsletter_id = $newsletter_id;
$queue->scheduled_at = $scheduled_at;
$queue->save();
expect($queue->getErrors())->false();
return $queue;
}
function _createNewsletter(
$type = Newsletter::TYPE_NOTIFICATION,
$status = Newsletter::STATUS_ACTIVE
) {
$newsletter = Newsletter::create();
$newsletter->type = $type;
$newsletter->status = $status;
$newsletter->save();
expect($newsletter->getErrors())->false();
return $newsletter;
}
function _createNewsletterOptions($newsletter_id, $newsletter_type, $options) {
foreach($options as $option => $value) {
$newsletter_option_field = NewsletterOptionField::create();
$newsletter_option_field->name = $option;
$newsletter_option_field->newsletter_type = $newsletter_type;
$newsletter_option_field->save();
expect($newsletter_option_field->getErrors())->false();
$newsletter_option = NewsletterOption::create();
$newsletter_option->option_field_id = $newsletter_option_field->id;
$newsletter_option->newsletter_id = $newsletter_id;
$newsletter_option->value = $value;
$newsletter_option->save();
expect($newsletter_option->getErrors())->false();
}
}
function _after() {
ORM::raw_execute('TRUNCATE ' . Newsletter::$_table);
ORM::raw_execute('TRUNCATE ' . NewsletterOption::$_table);
ORM::raw_execute('TRUNCATE ' . NewsletterOptionField::$_table);
ORM::raw_execute('TRUNCATE ' . NewsletterPost::$_table);
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);
}
}

View File

@@ -14,7 +14,7 @@ class FrontRouterTest extends MailPoetTest {
Router::NAME => '',
'endpoint' => 'mock_endpoint',
'action' => 'test',
'data' => base64_encode(serialize(array('data' => 'dummy data')))
'data' => base64_encode(json_encode(array('data' => 'dummy data')))
);
$this->router = new Router($this->router_data);
}
@@ -22,7 +22,7 @@ class FrontRouterTest extends MailPoetTest {
function testItCanGetAPIDataFromGetRequest() {
$data = array('data' => 'dummy data');
$url = 'http://example.com/?' . Router::NAME . '&endpoint=view_in_browser&action=view&data='
. base64_encode(serialize($data));
. base64_encode(json_encode($data));
parse_str(parse_url($url, PHP_URL_QUERY), $_GET);
$router = new Router();
expect($router->api_request)->equals(true);
@@ -100,7 +100,7 @@ class FrontRouterTest extends MailPoetTest {
$data = array('data' => 'dummy data');
$result = Router::encodeRequestData($data);
expect($result)->equals(
rtrim(base64_encode(serialize($data)), '=')
rtrim(base64_encode(json_encode($data)), '=')
);
}
@@ -112,14 +112,19 @@ class FrontRouterTest extends MailPoetTest {
function testItCanDecodeRequestData() {
$data = array('data' => 'dummy data');
$encoded_data = rtrim(base64_encode(serialize($data)), '=');
$encoded_data = rtrim(base64_encode(json_encode($data)), '=');
$result = Router::decodeRequestData($encoded_data);
expect($result)->equals($data);
}
function testItCanConvertInvalidRequestDataToArray() {
$result = Router::decodeRequestData('some_invalid_data');
expect($result)->equals(array());
}
function testItCanBuildRequest() {
$data = array('data' => 'dummy data');
$encoded_data = rtrim(base64_encode(serialize($data)), '=');
$encoded_data = rtrim(base64_encode(json_encode($data)), '=');
$result = Router::buildRequest(
'mock_endpoint',
'test',

View File

@@ -134,7 +134,7 @@ class ExportTest extends MailPoetTest {
expect(
preg_match(
'|' .
Env::$temp_path . '/MailPoet_export_[a-f0-9]{4}.' .
Env::$temp_path . '/MailPoet_export_[a-f0-9]{15}.' .
$this->export->export_format_option .
'|', $this->export->export_file)
)->equals(1);

View File

@@ -66,6 +66,31 @@ class ImportTest extends MailPoetTest {
expect($this->import->updated_at)->notEmpty();
}
function testItChecksForRequiredDataFields() {
$data = $this->data;
// exception should be thrown when one or more fields do not exist
unset($data['timestamp']);
try {
$this->import->validateData($data);
self::fail('Missing or invalid data exception not thrown.');
} catch(Exception $e) {
expect($e->getMessage())->equals('Missing or invalid subscriber data.');
}
// exception should not be thrown when all fields exist
$this->import->validateData($this->data);
}
function testItValidatesColumnNames() {
$data = $this->data;
$data['columns']['test) values ((ExtractValue(1,CONCAT(0x5c, (SELECT version())))))%23'] = true;
try {
$this->import->validateData($data);
self::fail('Missing or invalid data exception not thrown.');
} catch(Exception $e) {
expect($e->getMessage())->equals('Missing or invalid subscriber data.');
}
}
function testItCanTransformSubscribers() {
$custom_field = $this->subscriber_custom_fields[0];
expect($this->import->subscribers_data['first_name'][0])

View File

@@ -9,11 +9,22 @@ class MailChimpTest extends MailPoetTest {
$this->lists = explode(",", getenv('WP_TEST_IMPORT_MAILCHIMP_LISTS'));
}
function testItValidatesAPIKey() {
function testItCanGetAPIKey() {
$valid_api_key_format = '12345678901234567890123456789012-ab1';
// key must consist of two parts separated by hyphen
expect($this->mailchimp->getAPIKey('invalid_api_key_format'))->false();
// key must only contain numerals and letters
expect($this->mailchimp->getAPIKey('12345678901234567890123456789012-@?1'))->false();
// the first part of the key must contain 32 characters,
expect($this->mailchimp->getAPIKey('1234567890123456789012345678901-123'))
->false();
// the second part must contain 2 or 3 characters
expect($this->mailchimp->getAPIKey('12345678901234567890123456789012-1234'))
->false();
expect($this->mailchimp->getAPIKey('12345678901234567890123456789012-1'))
->false();
expect($this->mailchimp->getAPIKey($valid_api_key_format))
->equals($valid_api_key_format);
expect($this->mailchimp->getAPIKey('invalid_api_key_format'))->false();
}
function testItCanGetDatacenter() {

View File

@@ -1,4 +1,5 @@
<?php
use MailPoet\Router\Router;
use \MailPoet\Subscription\Url;
use \MailPoet\Models\Subscriber;
use \MailPoet\Models\Setting;
@@ -68,7 +69,7 @@ class UrlTest extends MailPoetTest {
// extract & decode data from url
$url_params = parse_url($url);
parse_str($url_params['query'], $params);
$data = unserialize(base64_decode($params['data']));
$data = Router::decodeRequestData($params['data']);
expect($data['email'])->contains('john@mailpoet.com');
expect($data['token'])->notEmpty();

View File

@@ -0,0 +1,34 @@
<?php
use Codeception\Util\Fixtures;
use Codeception\Util\Stub;
use MailPoet\Models\Subscriber;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
class SubscribersFeaturesTest extends MailPoetTest {
function testChecksIfSubscribersWithinLimitWhenPremiumLicenseDoesNotExist() {
$subscribers_feature = new SubscribersFeature();
expect($subscribers_feature->check(0))->false();
$subscriber = Subscriber::create();
$subscriber->hydrate(Fixtures::get('subscriber_template'));
$subscriber->save();
expect($subscribers_feature->check(0))->true();
}
function testChecksIfSubscribersWithinLimitWhenPremiumLicenseExists() {
$subscribers_feature = Stub::construct(
new SubscribersFeature(),
array(
'license' => true
)
);
$subscriber = Subscriber::create();
$subscriber->hydrate(Fixtures::get('subscriber_template'));
$subscriber->save();
expect($subscribers_feature->check(0))->false();
}
function _after() {
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
}
}

View File

@@ -0,0 +1,10 @@
<?php
use MailPoet\Util\License\License;
class LicenseTest extends MailPoetTest {
function testItGetsLicense() {
expect(License::getLicense())->false();
expect(License::getLicense('valid'))->equals('valid');
}
}

View File

@@ -39,8 +39,8 @@ jQuery('.toplevel_page_mailpoet-newsletters.menu-top-last')
<% block after_css %><% endblock %>
<script type="text/javascript">
var mailpoet_date_format = "<%= wp_datetime_format() %>";
var mailpoet_time_format = "<%= wp_time_format() %>";
var mailpoet_date_format = "<%= wp_datetime_format()|escape('js') %>";
var mailpoet_time_format = "<%= wp_time_format()|escape('js') %>";
</script>
<!-- javascripts -->

View File

@@ -322,8 +322,8 @@
'failedToFetchAvailablePosts': __('Failed to fetch available posts'),
'failedToFetchRenderedPosts': __('Failed to fetch rendered posts'),
'shortcodesWindowTitle': __('Select a shortcode'),
'unsubscribeLinkMissing': __('All newsletters must include an "Unsubscribe" link. Add a footer widget to your newsletter to continue'),
'newsletterPreviewEmailMissing': __('Enter an email address to send the preview newsletter to'),
'unsubscribeLinkMissing': __('All emails must include an "Unsubscribe" link. Add a footer widget to your email to continue.'),
'newsletterPreviewEmailMissing': __('Enter an email address to send the preview newsletter to.'),
'newsletterPreviewSent': __('Your test email has been sent!'),
'templateNameMissing': __('Please add a template name'),
'templateDescriptionMissing': __('Please add a template description'),
@@ -1209,7 +1209,7 @@
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }),
{ scroll: true }
{ scroll: true, static: true }
);
}
});

View File

@@ -1,2 +1,2 @@
<div class="mailpoet_container_empty">{{#ifCond emptyContainerMessage '!==' ''}}{{emptyContainerMessage}}{{else}}{{#if isRoot}}<%= __('Drop layout here!') %>{{else}}<%= __('Drop content here!') %>{{/if}}{{/ifCond}}</div>
<div class="mailpoet_container_empty">{{#ifCond emptyContainerMessage '!==' ''}}{{emptyContainerMessage}}{{else}}{{#if isRoot}}<%= __('Add a column block here.') %>{{else}}<%= __('Add a content block here.') %>{{/if}}{{/ifCond}}</div>
{{debug}}

View File

@@ -1,4 +1,4 @@
<h3><%= __('Layout') %></h3>
<h3><%= __('Columns') %></h3>
<div class="mailpoet_form_field">
<label>
<div class="mailpoet_form_field_input_option">

View File

@@ -1,4 +1,4 @@
<div class="handlediv" title="Click to toggle"><br></div>
<h3><%= __('Layout') %></h3>
<h3><%= __('Columns') %></h3>
<div class="mailpoet_region_content clearfix">
</div>

View File

@@ -20,9 +20,9 @@
<% block translations %>
<%= localize({
'pageTitle': __('Newsletters'),
'pageTitle': __('Emails'),
'tabStandardTitle': __('Newsletters'),
'tabStandardTitle': __('Emails'),
'tabWelcomeTitle': __('Welcome Emails'),
'tabNotificationTitle': __('Post Notifications'),
@@ -75,7 +75,7 @@
'trash': __('Trash'),
'edit': __('Edit'),
'duplicate': __('Duplicate'),
'newsletterDuplicated': __('Newsletter "%$1s" has been duplicated'),
'newsletterDuplicated': __('Email "%$1s" has been duplicated.'),
'notSentYet': __('Not sent yet'),
'scheduledFor': __('Scheduled for'),
'scheduleIt': __('Schedule it'),
@@ -97,18 +97,18 @@
'delete': __('Delete'),
'select': __('Select'),
'preview': __('Preview'),
'selectTemplateTitle': __('Select a template'),
'selectTemplateTitle': __('Select a responsive template'),
'draftNewsletterTitle': __('Subject'),
'pickCampaignType': __('Pick a type of campaign'),
'pickCampaignType': __('Select type of email'),
'regularNewsletterTypeTitle': __('Newsletter'),
'regularNewsletterTypeDescription': __('Send a newsletter with images, buttons, dividers, and social bookmarks. Or, just send a basic text email.'),
'create': __('Create'),
'welcomeNewsletterTypeTitle': __('Welcome Email'),
'welcomeNewsletterTypeDescription': __('Automatically send an email (or series of emails) to new subscribers or WordPress users. Send a day, a week, or a month after they sign up.'),
'setUp': __('Set up'),
'postNotificationNewsletterTypeTitle': __('Post Notifications'),
'postNotificationsNewsletterTypeDescription': __('Automatically send posts immediately, daily, weekly or monthly. Filter by categories, if you like.'),
'postNotificationNewsletterTypeTitle': __('Latest Post Notifications'),
'postNotificationsNewsletterTypeDescription': __('Let MailPoet email your subscribers with your latest content. You can send daily, weekly, monthly, or even immediately after publication.'),
'selectFrequency': __('Select a frequency'),
'postNotificationSubjectLineTip': __("Insert [newsletter:total] to show number of posts, [newsletter:post_title] to show the latest post's title & [newsletter:number] to display the issue number."),
'activate': __('Activate'),
@@ -146,7 +146,7 @@
'subjectLineTip': __("Be creative! It's the first thing that your subscribers see. Tempt them to open your email."),
'emptySubjectLineError': __('Please specify a subject'),
'segments': __('Lists'),
'segmentsTip': __('Your email newsletter(s) will be sent to this list.'),
'segmentsTip': __('This subscriber segment will be used for this email.'),
'selectSegmentPlaceholder': __('Select a list'),
'noSegmentsSelectedError': __('Please select a list'),
'sender': __('Sender'),
@@ -160,7 +160,7 @@
'newsletterUpdated': __('Newsletter was updated successfully!'),
'newsletterAdded': __('Newsletter was added successfully!'),
'newsletterSendingError': __('An error occurred while trying to send. <a href="%$1s">Please check your settings</a>'),
'finalNewsletterStep': __('Final step: last details'),
'finalNewsletterStep': __('Final Step: Last Details'),
'saveDraftAndClose': __('Save as draft and close'),
'orSimply': __('or simply'),
'goBackToDesign': __('go back to the Design page'),

View File

@@ -25,7 +25,7 @@
<tr>
<th scope="row">
<label>
<%= __('Newsletter task scheduler') %>
<%= __('Newsletter task scheduler (cron)') %>
</label>
<p class="description">
<%= __('Select what will activate your newsletter queue.') %>

View File

@@ -148,7 +148,7 @@
<%= __('Subscribe in registration form') %>
</label>
<p class="description">
<%= __('Allow users who register as a WordPress user on your website to subscribe to a MailPoet list (in addition to the "WordPress Users" list') %>
<%= __('Allow users who register as a WordPress user on your website to subscribe to a MailPoet list (in addition to the "WordPress Users" list)') %>
</p>
</th>
<td>
@@ -203,7 +203,7 @@
</div>
<% else %>
<p>
<em><%= __('Registration is disabled on this site') %></em>
<em><%= __('Registration is disabled on this site.') %></em>
</p>
<% endif %>
</td>
@@ -270,9 +270,9 @@
<%= __('Unsubscribe page') %>
</label>
<p class="description">
<%= __('When your subscribers click the "Unsubscribe" link, they will be directed to this page') %>
<%= __('When your subscribers click the "Unsubscribe" link, they will be directed to this page.') %>
<br />
<%= __('Use this shortcode on your website\'s WordPress pages: [mailpoet_manage text="Manage your subscription"]') %>
<%= __('If you want to use a custom Unsubscribe page, simply paste this shortcode on to a WordPress page: [mailpoet_manage_text="Manage your subscription"]') %>
</p>
</th>
<td>
@@ -340,7 +340,7 @@
<tr>
<th scope="row">
<label>
<%= __('Shortcode to Display Total Number of Subscribers') %>
<%= __('Shortcode to display total number of subscribers') %>
</label>
<p class="description">
<%= __('Paste this shortcode on a post or page to display the total number of confirmed subscribers') %>

View File

@@ -194,7 +194,7 @@
if(~~($(this).val()) === 1) {
result = confirm("<%= __('Subscribers will need to activate their subscription via email in order to receive your newsletters. This is highly recommended!') %>");
} else {
result = confirm("<%= __('Unconfirmed subscribers will receive your newsletters from without needing to activate their subscriptions. This is not recommended!') %>");
result = confirm("<%= __('New subscribers will be automatically confirmed, without having to confirm their subscription. This is not recommended!') %>");
}
// if the user confirmed changing the signup confirmation (yes/no)
if(result === true) {

View File

@@ -52,7 +52,7 @@
'userColumns': __('User fields'),
'selectedValueAlreadyMatched': __('The selected value is already matched to another field'),
'confirmCorrespondingColumn': __('Confirm that this field corresponds to the selected field'),
'columnContainInvalidElement': __('One of the fields contains an invalid email. Please fix it before continuing'),
'columnContainInvalidElement': __('One of the fields contains an invalid email. Please fix it before continuing.'),
'january': __('January'),
'february': __('February'),
'march': __('March'),
@@ -65,15 +65,15 @@
'october': __('October'),
'november': __('November'),
'december': __('December'),
'noDateFieldMatch': __("Do not match as a 'date field' if most of the rows for that field return the same error"),
'emptyFirstRowDate': __('First row date cannot be empty'),
'noDateFieldMatch': __("Do not match as a 'date field' if most of the rows for that field return the same error."),
'emptyFirstRowDate': __('First row date cannot be empty.'),
'verifyDateMatch': __('Verify that the date in blue matches the original date'),
'pm': __('PM'),
'am': __('AM'),
'dateMatchError': __('Error matching date'),
'columnContainsInvalidDate': __('One of the fields contains an invalid date. Please fix it before continuing'),
'columnContainsInvalidDate': __('One of the fields contains an invalid date. Please fix before continuing.'),
'listCreateError': __('Error adding a new list:'),
'columnContainsInvalidElement': __('One of the fields contains an invalid email. Please fix before continuing'),
'columnContainsInvalidElement': __('One of the fields contains an invalid email. Please fix before continuing.'),
'customFieldCreateError': __('Custom field could not be created'),
'subscribersCreated': __('%1$s subscribers added to %2$s.'),
'subscribersUpdated': __('%1$s existing subscribers were updated and added to %2$s')

View File

@@ -116,11 +116,11 @@
{{#subscribers}}
<tr>
<td>
{{show_real_index @index}}
{{calculate_index @index}}
</td>
{{#.}}
<td>
{{{this}}}
{{sanitize_data this}}
</td>
{{/.}}
</tr>

View File

@@ -17,7 +17,7 @@
'pageTitle': __('Subscribers'),
'searchLabel': __('Search'),
'loadingItems': __('Loading subscribers...'),
'noItemsFound': __('No subscribers were found'),
'noItemsFound': __('No subscribers were found.'),
'selectAllLabel': __('All subscribers on this page are selected.'),
'selectedAllLabel': __('All %d subscribers are selected'),
'selectAllLink': __('Select all subscribers on all pages.'),

View File

@@ -17,31 +17,15 @@
<div style="position: absolute; top: .2em; right: 0;"><img src="<%= image_url('welcome_template/mailpoet-logo.png') %>" alt="MailPoet Logo" /></div>
<h2 class="nav-tab-wrapper wp-clearfix">
<a href="admin.php?page=mailpoet-welcome" class="nav-tab"><%= __('Whats New') %></a>
<a href="admin.php?page=mailpoet-update" class="nav-tab nav-tab-active"><%= __('Changelog') %></a>
<a href="admin.php?page=mailpoet-welcome" class="nav-tab"><%= __('Welcome') %></a>
<a href="admin.php?page=mailpoet-update" class="nav-tab nav-tab-active"><%= __("What's New") %></a>
</h2>
<div id="mailpoet-changelog" clas="feature-section one-col">
<h2><%= __("List of Changes") %></h2>
<h3>0.0.48 - 2016-10-11</h3>
<h3>3.0.0-beta.1 - 2016-10-28</h3>
<ul>
<li>Added `mailpoet` text domain to gettext translation functions;</li>
<li>Added `.pot` translation template file generation to build process;</li>
<li>Fixed SQL injection via listings in admin panel;</li>
<li>Fixed stored XSS in Idiorm library demo code;</li>
<li>Fixed constant usage before initialization errors;</li>
<li>Fixed subscriber token leak via timing attacks;</li>
<li>Added a "Read More" link to "WordPress Users" list in "Lists" admin listing;</li>
<li>Removed test code and docs from vendor code in our distributable zip.</li>
</ul>
<br>
<h3>0.0.47 - 2016-10-04</h3>
<ul>
<li>Fixed subscription form to not send confirmation email when sending one is disabled in settings;</li>
<li>Fixed segment selection field to preselect previously used segments on last newsletter creation step;</li>
<li>Fixed segment subscriber count to be always displayed;</li>
<li>Changed segment subscriber count to not include unsubscribed or unconfirmed subscribers.</li>
<li>Initial public beta release;</li>
</ul>
<br>
@@ -51,7 +35,7 @@
<div clas="feature-section one-col">
<br>
<p style="text-align: center"><a class="button button-primary" href="https://wordpress.org/plugins/wysija-newsletters/changelog/" target="_blank"><%= __("View all changes") %> &rarr;</a></p>
<p style="text-align: center"><a class="button button-primary" href="https://wordpress.org/plugins/mailpoet/changelog/" target="_blank"><%= __("View all changes") %> &rarr;</a></p>
</div>
</div>

View File

@@ -26,8 +26,8 @@
<div style="position: absolute; top: .2em; right: 0;"><img src="<%= image_url('welcome_template/mailpoet-logo.png') %>" alt="<%= __('MailPoet Logo') %>" /></div>
<h2 class="nav-tab-wrapper wp-clearfix">
<a href="admin.php?page=mailpoet-welcome" class="nav-tab nav-tab-active"><%= __('Whats New') %></a>
<a href="admin.php?page=mailpoet-update" class="nav-tab"><%= __('Changelog') %></a>
<a href="admin.php?page=mailpoet-welcome" class="nav-tab nav-tab-active"><%= __('Welcome') %></a>
<a href="admin.php?page=mailpoet-update" class="nav-tab"><%= __("What's new") %></a>
</h2>
<div class="headline-feature feature-video">