Compare commits

...

31 Commits

Author SHA1 Message Date
a03891895c Bump up release version to 0.0.50 and update changelog 2016-10-25 13:04:55 +03:00
3368e84a99 Merge pull request #668 from mailpoet/export_confirmed_subscribers_option_update
Fixes minor export UI issues
2016-10-25 12:46:13 +03:00
e90df2f08d - Fixes Select2 not dislaying multiple options in the list of export
fields
- Sets default "export confirmed subscriber" option to "no"
2016-10-24 13:09:48 -04:00
2391ae1cad Merge pull request #665 from mailpoet/post_notification_fix
Fixes post notification issues
2016-10-24 16:02:19 +03:00
83114a8be4 - Removes unused class declarations 2016-10-24 08:55:22 -04:00
d08d5a3b6c - Updates unit tests 2016-10-24 08:55:22 -04:00
8330bfc884 - Fixes "completed" status update of notification history
newsletters
- Fixes detection of post notification newsletters that do not contain any posts (i.e., blank ALC blocks)
- Updates unit test
2016-10-24 08:55:22 -04:00
ef21a8cca7 - Enables post notification schedule update upon newsletter saving during
step 3
2016-10-24 08:55:22 -04:00
e32c46a755 - Detaches posts_where action after posts are pulled from the database 2016-10-24 08:55:22 -04:00
092f69538a Merge pull request #667 from mailpoet/sending_to_trashed_subscribers_fix
Prevents newsletters from being sent to trashed subscribers
2016-10-24 15:26:55 +03:00
7a75367d75 Merge pull request #666 from mailpoet/export_filename_update
Increases export filename length and randomness
2016-10-24 13:36:36 +03:00
0b2701ade2 Merge pull request #656 from mailpoet/security_issue_636
API Token
2016-10-24 13:26:44 +03:00
1ac288d286 - Prevents newsletters from being sent to trashed subscribers
- Updates unit tests
- Addresses #629
2016-10-21 14:36:44 -04:00
516bc73092 - Increases export filename length and randomness 2016-10-21 11:42:13 -04:00
4088abef68 removed useless 'use' in unit test 2016-10-21 13:42:19 +02:00
f6cefc3f5c wrong email address in unit test 2016-10-21 13:38:23 +02:00
202e4b90e1 added unit test for API::checkPermissions 2016-10-21 13:36:41 +02:00
ee89bf0722 refactored API class 2016-10-21 13:36:41 +02:00
876d21300a fixed duplicated lines due to faulty rebase 2016-10-21 13:36:41 +02:00
0ca5b7a79f API Security
- added APIAccess class to define access levels of API Endpoints (permissions)
- use "mailpoet_token" for all nonce (just as before)
- merged setupPublic/setupAdmin methods in API in order to avoid duplication
- check permission if access level is not all
- fixed ABSPATH check in some classes
2016-10-21 13:36:41 +02:00
5d0ee43921 removed checkToken for admin ajax 2016-10-21 13:36:41 +02:00
cc523a3c0b ability to specify action for generateToken() method 2016-10-21 13:36:41 +02:00
2787998d32 Merge pull request #664 from mailpoet/editor_fixes
Editor fixes
2016-10-20 17:29:55 +02:00
38f6c95059 Update newsletter saving to reflect code review comments
- Switch to using full segment objects when saving newsletters
- Fix stale comment in newsletter editor's Newsletter model
- Fix typo in newsletter editor tests
2016-10-20 17:52:05 +03:00
cc03b631ff Allow newsletters.save endpoint to accept segments as list of objects 2016-10-20 16:08:41 +03:00
a3c77fb685 Fix PHP to JS date format converter to handle escaped symbols 2016-10-20 15:19:04 +03:00
3817e28960 Change newsletter not found error to a static one in editor 2016-10-20 13:38:07 +03:00
c3a78b1ea3 Fix newsletter editor to only save properties it changes 2016-10-20 13:37:32 +03:00
42877236c8 Merge pull request #663 from mailpoet/wp_repo_files
Preparation for plugin repo
2016-10-19 09:08:54 -04:00
6e87f3539c Update license.txt, readme.txt and link to plugin's repo page 2016-10-19 13:46:14 +03:00
7704ea4b68 Bump up release version to 0.0.49 2016-10-19 13:23:00 +03:00
40 changed files with 597 additions and 207 deletions

View File

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

View File

@ -122,9 +122,10 @@ function(
} else { } else {
value = e.target.value; value = e.target.value;
} }
var transformedValue = this.transformChangedValue(value);
this.props.onValueChange({ this.props.onValueChange({
target: { target: {
value: value, value: transformedValue,
name: this.props.field.name name: this.props.field.name
} }
}); });
@ -148,6 +149,16 @@ function(
} }
return item.id; 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() { render: function() {
const options = this.state.items.map((item, index) => { const options = this.state.items.map((item, index) => {
let label = this.getLabel(item); let label = this.getLabel(item);

View File

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

View File

@ -1,11 +1,13 @@
define( define(
[ [
'mailpoet', 'mailpoet',
'newsletters/types/notification/scheduling.jsx' 'newsletters/types/notification/scheduling.jsx',
'underscore'
], ],
function( function(
MailPoet, MailPoet,
Scheduling Scheduling,
_
) { ) {
var settings = window.mailpoet_settings || {}; var settings = window.mailpoet_settings || {};
@ -42,6 +44,14 @@ define(
getLabel: function(segment) { getLabel: function(segment) {
return segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')'; 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: { validation: {
'data-parsley-required': true, 'data-parsley-required': true,
'data-parsley-required-message': MailPoet.I18n.t('noSegmentsSelectedError') 'data-parsley-required-message': MailPoet.I18n.t('noSegmentsSelectedError')

View File

@ -348,6 +348,14 @@ define(
getLabel: function(segment) { getLabel: function(segment) {
return segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')'; 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: { validation: {
'data-parsley-required': true, 'data-parsley-required': true,
'data-parsley-required-message': MailPoet.I18n.t('noSegmentsSelectedError') 'data-parsley-required-message': MailPoet.I18n.t('noSegmentsSelectedError')

View File

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

View File

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

View File

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

View File

@ -5,31 +5,40 @@ use \MailPoet\Util\Security;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class API { class API {
private $_endpoint;
private $_method;
private $_token;
private $_endpoint_class;
private $_data = array();
function init() { function init() {
// security token // Admin Security token
add_action( add_action(
'admin_head', 'admin_head',
array($this, 'setToken') array($this, 'setToken')
); );
// Admin API (Ajax only) // ajax (logged in users)
add_action( add_action(
'wp_ajax_mailpoet', 'wp_ajax_mailpoet',
array($this, 'setupAdmin') array($this, 'setupAjax')
); );
// Public API (Ajax) // ajax (logged out users)
add_action( add_action(
'wp_ajax_nopriv_mailpoet', 'wp_ajax_nopriv_mailpoet',
array($this, 'setupPublic') array($this, 'setupAjax')
); );
} }
function setupAdmin() { function setupAjax() {
$this->getRequestData();
if($this->checkToken() === false) { if($this->checkToken() === false) {
$error_response = new ErrorResponse( $error_response = new ErrorResponse(
array( array(
Error::UNAUTHORIZED => __('You need to specify a valid API token.', 'mailpoet') Error::UNAUTHORIZED => __('Invalid request.', 'mailpoet')
), ),
array(), array(),
Response::STATUS_UNAUTHORIZED Response::STATUS_UNAUTHORIZED
@ -37,55 +46,84 @@ class API {
$error_response->send(); $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(); $this->processRoute();
} }
function setupPublic() { function getRequestData() {
if($this->checkToken() === false) { $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( $error_response = new ErrorResponse(
array( array(
Error::UNAUTHORIZED => __('You need to specify a valid API token.', 'mailpoet') Error::BAD_REQUEST => __('Invalid request.', 'mailpoet')
), ),
array(), array(),
Response::STATUS_UNAUTHORIZED Response::STATUS_BAD_REQUEST
); );
$error_response->send(); $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() { 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 { try {
$endpoint = new $endpoint(); $endpoint = new $this->_endpoint_class();
$response = $endpoint->$method($data);
// 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(); $response->send();
} catch(\Exception $e) { } catch(\Exception $e) {
$error_response = new ErrorResponse( $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() { function checkPermissions() {
return current_user_can('manage_options'); return current_user_can('manage_options');
} }
function checkToken() { function checkToken() {
return ( return wp_verify_nonce($this->_token, 'mailpoet_token');
isset($_POST['token']) }
&&
wp_verify_nonce($_POST['token'], 'mailpoet_token') function setToken() {
); $global = '<script type="text/javascript">';
$global .= 'var mailpoet_token = "';
$global .= Security::generateToken();
$global .= '";';
$global .= '</script>';
echo $global;
} }
} }

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,12 +15,6 @@ class AutomatedLatestContent {
function __construct($newsletter_id = false, $newer_than_timestamp = false) { function __construct($newsletter_id = false, $newer_than_timestamp = false) {
$this->newsletter_id = $newsletter_id; $this->newsletter_id = $newsletter_id;
$this->newer_than_timestamp = $newer_than_timestamp; $this->newer_than_timestamp = $newer_than_timestamp;
$this->_attachSentPostsFilter();
}
function __destruct() {
$this->_detachSentPostsFilter();
} }
function filterOutSentPosts($where) { 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) { function transformPosts($args, $posts) {
@ -129,4 +126,4 @@ class AutomatedLatestContent {
remove_action('posts_where', array($this, 'filterOutSentPosts')); remove_action('posts_where', array($this, 'filterOutSentPosts'));
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,41 +1,89 @@
=== MailPoet === === MailPoet 3 - Beta Version ===
Contributors: mailpoet Contributors: mailpoet
Donate link: http://mailpoet.com Tags: newsletter, email, welcome email, post notification, autoresponder, mailchimp, signup, smtp
Tags: wordpress, plugin Requires at least: 4.6
Requires at least: 4.0 Tested up to: 4.6
Tested up to: 4.0 Stable tag: 3.0.0
Stable tag: 1.0 Create and send beautiful emails and newsletters from WordPress.
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html == Description ==
MailPoet newsletters. Try the new MailPoet! This is a beta version of our completely new email newsletter plugin.(https://wordpress.org/plugins/wysija-newsletters/).
== Description == = What's new? =
Long description. * New email designer
* Responsive templates
== Installation == * Send with MailPoet's sending service
* Fast user interface
Installation instructions. * Easier initial configuration
== Screenshots == [Try the demo.](http://demo3.mailpoet.com/launch/)
Screenshots. = Check out this 2 minute video. =
== Frequently Asked Questions == [vimeo https://vimeo.com/183339372]
= Question? = = Use at your own risk! =
Answer. Use [the current stable MailPoet](https://wordpress.org/plugins/wysija-newsletters/) instead of this version if you are not a power user.
== Changelog == * This beta version is for testing purposes only!
* Not RTL compatible
= 1.0 = * We expect bug reports from you!
* 2020-01-01 * Multisite not supported
* Initial release * No migration script from MailPoet 2.X to this version
* Weekly releases
== Upgrade Notice ==
= Premium version =
= 1.0 =
* 2020-01-01 Not available yet. Limited stats in free version.
* Initial release
= 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 - 2016-09 =
* Hello world.

View File

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

View File

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

View File

@ -1,11 +1,11 @@
<?php <?php
use \MailPoet\API\Response as APIResponse; use \MailPoet\API\Response as APIResponse;
use \MailPoet\API\Error as APIError;
use \MailPoet\API\Endpoints\Newsletters; use \MailPoet\API\Endpoints\Newsletters;
use \MailPoet\Models\Newsletter; use \MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterOptionField;
use \MailPoet\Models\NewsletterSegment; use \MailPoet\Models\NewsletterSegment;
use \MailPoet\Models\NewsletterTemplate;
use \MailPoet\Models\Segment; use \MailPoet\Models\Segment;
use MailPoet\Newsletter\Scheduler\Scheduler;
class NewslettersTest extends MailPoetTest { class NewslettersTest extends MailPoetTest {
function _before() { function _before() {
@ -44,17 +44,26 @@ class NewslettersTest extends MailPoetTest {
} }
function testItCanSaveANewNewsletter() { 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( $valid_data = array(
'subject' => 'My First Newsletter', 'subject' => 'My First Newsletter',
'type' => Newsletter::TYPE_STANDARD 'type' => Newsletter::TYPE_STANDARD,
'options' => array(
$newsletter_option_field->name => 'some_option_value',
)
); );
$router = new Newsletters(); $router = new Newsletters();
$response = $router->save($valid_data); $response = $router->save($valid_data);
$saved_newsletter = Newsletter::filter('filterWithOptions')->findOne($response->data['id']);
expect($response->status)->equals(APIResponse::STATUS_OK); expect($response->status)->equals(APIResponse::STATUS_OK);
expect($response->data)->equals( expect($response->data)->equals($saved_newsletter->asArray());
Newsletter::findOne($response->data['id'])->asArray() // newsletter option should be saved
); expect($saved_newsletter->some_option)->equals('some_option_value');
$invalid_data = array( $invalid_data = array(
'subject' => 'Missing newsletter type' 'subject' => 'Missing newsletter type'
@ -73,13 +82,75 @@ class NewslettersTest extends MailPoetTest {
); );
$response = $router->save($newsletter_data); $response = $router->save($newsletter_data);
$updated_newsletter = Newsletter::findOne($this->newsletter->id);
expect($response->status)->equals(APIResponse::STATUS_OK); expect($response->status)->equals(APIResponse::STATUS_OK);
expect($response->data)->equals( expect($response->data)->equals($updated_newsletter->asArray());
Newsletter::findOne($this->newsletter->id)->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); $response = $router->save($newsletter_data);
expect($updated_newsletter->subject)->equals('My Updated Newsletter'); 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() { function testItCanSetANewsletterStatus() {
@ -377,6 +448,7 @@ class NewslettersTest extends MailPoetTest {
function _after() { function _after() {
Newsletter::deleteMany(); Newsletter::deleteMany();
NewsletterSegment::deleteMany(); NewsletterSegment::deleteMany();
NewsletterOptionField::deleteMany();
Segment::deleteMany(); Segment::deleteMany();
} }
} }

View File

@ -1,5 +1,6 @@
<?php <?php
use Carbon\Carbon;
use Codeception\Util\Fixtures; use Codeception\Util\Fixtures;
use Codeception\Util\Stub; use Codeception\Util\Stub;
use MailPoet\API\Endpoints\Cron; use MailPoet\API\Endpoints\Cron;
@ -298,6 +299,29 @@ class SendingQueueTest extends MailPoetTest {
expect($statistics)->false(); 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() { function _after() {
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table); ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table); ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);

View File

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

View File

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

View File

@ -134,7 +134,7 @@ class ExportTest extends MailPoetTest {
expect( expect(
preg_match( 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_format_option .
'|', $this->export->export_file) '|', $this->export->export_file)
)->equals(1); )->equals(1);

View File

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

View File

@ -1209,7 +1209,7 @@
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }), response.errors.map(function(error) { return error.message; }),
{ scroll: true } { scroll: true, static: true }
); );
} }
}); });

View File

@ -23,25 +23,31 @@
<div id="mailpoet-changelog" clas="feature-section one-col"> <div id="mailpoet-changelog" clas="feature-section one-col">
<h2><%= __("List of Changes") %></h2> <h2><%= __("List of Changes") %></h2>
<h3>0.0.48 - 2016-10-11</h3> <h3>0.0.50 - 2016-10-25</h3>
<ul> <ul>
<li>Added `mailpoet` text domain to gettext translation functions;</li> <li>Renamed "LICENSE" to "license.txt" in preparation for WP plugin repo;</li>
<li>Added `.pot` translation template file generation to build process;</li> <li>Updated "readme.txt" with newly prepared README text;</li>
<li>Fixed SQL injection via listings in admin panel;</li> <li>Updated "View All Changes" button on plugin update page to link to MailPoet plugin repo page;</li>
<li>Fixed stored XSS in Idiorm library demo code;</li> <li>Fixed date formatting to allow using escaped symbols;</li>
<li>Fixed constant usage before initialization errors;</li> <li>Fixed saving existing newsletters in newsletter editor and last newsletter creation step;</li>
<li>Fixed subscriber token leak via timing attacks;</li> <li>Changed "Newsletter not found" error on newsletter editor page to be displayed permanently;</li>
<li>Added a "Read More" link to "WordPress Users" list in "Lists" admin listing;</li> <li>Changed "newsletters.save" endpoint to require segment objects when saving newsletters;</li>
<li>Removed test code and docs from vendor code in our distributable zip.</li> <li>Fixed security issue with public token reuse on admin panel;</li>
<li>Added endpoint specific access limits;</li>
<li>Increased subscriber export filename length and complexity to make it more difficult to guess file names;</li>
<li>Fixed sending queue to not send newsletters to trashed subscribers;</li>
<li>Fixed post notifications to work properly with multiple notification newsletters sent at the same time;</li>
<li>Fixed post notification newsletters to correctly set newsletter status once sending is completed;</li>
<li>Fixed post notification newsletters to not send newsletters without any ALC posts;</li>
<li>Fixed selecting multiple data fields in subscriber export.</li>
</ul> </ul>
<br> <br>
<h3>0.0.47 - 2016-10-04</h3> <h3>0.0.49 - 2016-10-18</h3>
<ul> <ul>
<li>Fixed subscription form to not send confirmation email when sending one is disabled in settings;</li> <li>Fixed security issues in Front Router, subscriber import, newsletter preview, admin listings, Idiorm demo code and subscriber verification;</li>
<li>Fixed segment selection field to preselect previously used segments on last newsletter creation step;</li> <li>Added unit tests for newsletter scheduler;</li>
<li>Fixed segment subscriber count to be always displayed;</li> <li>Added "Read more" documentation URL describing "WordPress Users" list in admin listings.</li>
<li>Changed segment subscriber count to not include unsubscribed or unconfirmed subscribers.</li>
</ul> </ul>
<br> <br>
@ -51,7 +57,7 @@
<div clas="feature-section one-col"> <div clas="feature-section one-col">
<br> <br>
<p style="text-align: center"><a class="button button-primary" href="https://wordpress.org/plugins/wysija-newsletters/changelog/" target="_blank"><%= __("View all changes") %> &rarr;</a></p> <p style="text-align: center"><a class="button button-primary" href="https://wordpress.org/plugins/mailpoet/changelog/" target="_blank"><%= __("View all changes") %> &rarr;</a></p>
</div> </div>
</div> </div>