Compare commits
62 Commits
0.0.48
...
3.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
|
b2d4bfc760 | ||
|
57f5f16bb6 | ||
|
7d2e13b9a3 | ||
|
6d39f9fa78 | ||
|
a4395f2350 | ||
|
411969b3eb | ||
|
1868ca3155 | ||
|
e765471f5d | ||
|
bdce7c5e5a | ||
|
773be9f5c8 | ||
|
6ae46b05e5 | ||
|
217894745d | ||
|
a03891895c | ||
|
3368e84a99 | ||
|
e90df2f08d | ||
|
2391ae1cad | ||
|
83114a8be4 | ||
|
d08d5a3b6c | ||
|
8330bfc884 | ||
|
ef21a8cca7 | ||
|
e32c46a755 | ||
|
092f69538a | ||
|
7a75367d75 | ||
|
0b2701ade2 | ||
|
1ac288d286 | ||
|
516bc73092 | ||
|
4088abef68 | ||
|
f6cefc3f5c | ||
|
202e4b90e1 | ||
|
ee89bf0722 | ||
|
876d21300a | ||
|
0ca5b7a79f | ||
|
5d0ee43921 | ||
|
cc523a3c0b | ||
|
2787998d32 | ||
|
38f6c95059 | ||
|
cc03b631ff | ||
|
a3c77fb685 | ||
|
3817e28960 | ||
|
c3a78b1ea3 | ||
|
42877236c8 | ||
|
6e87f3539c | ||
|
7704ea4b68 | ||
|
12a3931b7b | ||
|
25a55dbb67 | ||
|
6758f60a81 | ||
|
5e9e53ec41 | ||
|
1285252a8c | ||
|
98f95f72ad | ||
|
09ca788371 | ||
|
b48cc5a959 | ||
|
812d138c4e | ||
|
07bc35d4cd | ||
|
90b95a2c25 | ||
|
78c50c41e3 | ||
|
7eee7def63 | ||
|
9ba6e9806f | ||
|
8c28dc3d8a | ||
|
9197e39fb4 | ||
|
37f59814e5 | ||
|
e565a7a234 | ||
|
e1c5f609ff |
@@ -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('');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -62,7 +62,7 @@ const _monthDayValues = _.object(
|
||||
} else {
|
||||
label = MailPoet.I18n.t('nth').replace("%$1d", day + 1);
|
||||
}
|
||||
return [day, label];
|
||||
return [day + 1, label];
|
||||
}
|
||||
)
|
||||
);
|
||||
|
@@ -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')
|
||||
|
@@ -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')
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import _ from 'underscore'
|
||||
import React from 'react'
|
||||
import MailPoet from 'mailpoet'
|
||||
import Select from 'form/fields/select.jsx'
|
||||
import {
|
||||
intervalValues,
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
};
|
||||
});
|
||||
|
2
build.sh
@@ -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
|
||||
|
144
lib/API/API.php
@@ -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
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace MailPoet\API;
|
||||
|
||||
if(!defined('ABSPATH')) exit;
|
||||
|
||||
final class Access {
|
||||
const ALL = 'all';
|
||||
|
||||
private function __construct() {
|
||||
|
||||
}
|
||||
}
|
@@ -5,6 +5,8 @@ if(!defined('ABSPATH')) exit;
|
||||
|
||||
abstract class Endpoint {
|
||||
|
||||
public $permissions = array();
|
||||
|
||||
function successResponse(
|
||||
$data = array(), $meta = array(), $status = Response::STATUS_OK
|
||||
) {
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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_';
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -383,6 +383,7 @@ class Subscriber extends Model {
|
||||
'subscribers.id = relation.subscriber_id',
|
||||
'subscribers'
|
||||
)
|
||||
->whereNull('subscribers.deleted_at')
|
||||
->where('subscribers.status', 'subscribed');
|
||||
return $subscribers;
|
||||
}
|
||||
|
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
|
@@ -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 {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
200
lib/Util/CSS.php
@@ -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);
|
||||
|
19
lib/Util/License/Features/Subscribers.php
Normal 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;
|
||||
}
|
||||
}
|
13
lib/Util/License/License.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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/
|
@@ -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__,
|
||||
|
BIN
plugin_repository/assets/banner-1500x500.jpg
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
plugin_repository/assets/banner-772x250.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
plugin_repository/assets/icon-128x128.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
plugin_repository/assets/screenshot-1.png
Normal file
After Width: | Height: | Size: 129 KiB |
BIN
plugin_repository/assets/screenshot-2.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
plugin_repository/assets/screenshot-3.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
plugin_repository/assets/screenshot-4.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
plugin_repository/assets/screenshot-5.png
Normal file
After Width: | Height: | Size: 46 KiB |
130
readme.txt
@@ -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.
|
||||
|
||||
|
@@ -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');
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
38
tests/unit/API/APITest.php
Normal 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);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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');
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
);
|
||||
|
||||
|
455
tests/unit/Newsletter/Scheduler/SchedulerTest.php
Normal 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);
|
||||
}
|
||||
}
|
@@ -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',
|
||||
|
@@ -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);
|
||||
|
@@ -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])
|
||||
|
@@ -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() {
|
||||
|
@@ -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();
|
||||
|
34
tests/unit/Util/License/Features/SubscribersTest.php
Normal 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);
|
||||
}
|
||||
}
|
10
tests/unit/Util/License/LicenseTest.php
Normal 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');
|
||||
}
|
||||
}
|
@@ -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 -->
|
||||
|
@@ -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 }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@@ -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}}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<h3><%= __('Layout') %></h3>
|
||||
<h3><%= __('Columns') %></h3>
|
||||
<div class="mailpoet_form_field">
|
||||
<label>
|
||||
<div class="mailpoet_form_field_input_option">
|
||||
|
@@ -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>
|
||||
|
@@ -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'),
|
||||
|
@@ -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.') %>
|
||||
|
@@ -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') %>
|
||||
|
@@ -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) {
|
||||
|
@@ -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')
|
||||
|
@@ -116,11 +116,11 @@
|
||||
{{#subscribers}}
|
||||
<tr>
|
||||
<td>
|
||||
{{show_real_index @index}}
|
||||
{{calculate_index @index}}
|
||||
</td>
|
||||
{{#.}}
|
||||
<td>
|
||||
{{{this}}}
|
||||
{{sanitize_data this}}
|
||||
</td>
|
||||
{{/.}}
|
||||
</tr>
|
||||
|
@@ -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.'),
|
||||
|
@@ -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"><%= __('What’s 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") %> →</a></p>
|
||||
<p style="text-align: center"><a class="button button-primary" href="https://wordpress.org/plugins/mailpoet/changelog/" target="_blank"><%= __("View all changes") %> →</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -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"><%= __('What’s 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">
|
||||
|