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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ if(!defined('ABSPATH')) exit;
use \MailPoet\Config\Initializer;
/*
* Plugin Name: MailPoet
* Version: 0.0.48
* Version: 0.0.50
* 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', '0.0.50');
$initializer = new Initializer(array(
'file' => __FILE__,

View File

@ -1,41 +1,89 @@
=== MailPoet ===
Contributors: mailpoet
Donate link: http://mailpoet.com
Tags: wordpress, plugin
Requires at least: 4.0
Tested up to: 4.0
Stable tag: 1.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
MailPoet newsletters.
== Description ==
Long description.
== Installation ==
Installation instructions.
== Screenshots ==
Screenshots.
== Frequently Asked Questions ==
= Question? =
Answer.
== Changelog ==
= 1.0 =
* 2020-01-01
* Initial release
== Upgrade Notice ==
= 1.0 =
* 2020-01-01
* Initial release
=== MailPoet 3 - Beta Version ===
Contributors: mailpoet
Tags: newsletter, email, welcome email, post notification, autoresponder, mailchimp, signup, smtp
Requires at least: 4.6
Tested up to: 4.6
Stable tag: 3.0.0
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 - 2016-09 =
* Hello world.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,25 +23,31 @@
<div id="mailpoet-changelog" clas="feature-section one-col">
<h2><%= __("List of Changes") %></h2>
<h3>0.0.48 - 2016-10-11</h3>
<h3>0.0.50 - 2016-10-25</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>
<li>Renamed "LICENSE" to "license.txt" in preparation for WP plugin repo;</li>
<li>Updated "readme.txt" with newly prepared README text;</li>
<li>Updated "View All Changes" button on plugin update page to link to MailPoet plugin repo page;</li>
<li>Fixed date formatting to allow using escaped symbols;</li>
<li>Fixed saving existing newsletters in newsletter editor and last newsletter creation step;</li>
<li>Changed "Newsletter not found" error on newsletter editor page to be displayed permanently;</li>
<li>Changed "newsletters.save" endpoint to require segment objects when saving newsletters;</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>
<br>
<h3>0.0.47 - 2016-10-04</h3>
<h3>0.0.49 - 2016-10-18</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>Fixed security issues in Front Router, subscriber import, newsletter preview, admin listings, Idiorm demo code and subscriber verification;</li>
<li>Added unit tests for newsletter scheduler;</li>
<li>Added "Read more" documentation URL describing "WordPress Users" list in admin listings.</li>
</ul>
<br>
@ -51,7 +57,7 @@
<div clas="feature-section one-col">
<br>
<p style="text-align: center"><a class="button button-primary" href="https://wordpress.org/plugins/wysija-newsletters/changelog/" target="_blank"><%= __("View all changes") %> &rarr;</a></p>
<p style="text-align: center"><a class="button button-primary" href="https://wordpress.org/plugins/mailpoet/changelog/" target="_blank"><%= __("View all changes") %> &rarr;</a></p>
</div>
</div>