Compare commits

..

81 Commits

Author SHA1 Message Date
d2b41a5b90 - Bumps up release version to 3.0.0-beta.10
- Updates changelog
2016-12-27 17:29:42 -05:00
71db3e569d Merge pull request #759 from mailpoet/deleted_lists_fix
Show deleted lists in newsletter listings [MAILPOET-489]
2016-12-27 11:26:59 -05:00
9a8f028a01 Fix code style [MAILPOET-489] 2016-12-27 19:16:18 +03:00
2c5e73305a Show deleted lists in newsletter listings [MAILPOET-489] 2016-12-27 13:13:05 +03:00
90e7026355 Merge pull request #739 from mailpoet/archives_shortcode_fix
Fixes archives shortcode and ability to manage subscription/unsubscribe [MAILPOET-737]
2016-12-27 09:39:15 +03:00
d63ab6a927 - Uses newsletter hash over id when looking for a newsletter 2016-12-26 19:42:00 -05:00
e5cf57e4f8 Merge pull request #756 from mailpoet/save_before_send_preview
Save newsletter before sending preview [MAILPOET-702]
2016-12-25 16:17:41 -05:00
6beada63de - Allows administrators to preview all newsletters
- Allows non-administrators to preview newsletters only when newsletter
hash is specified
2016-12-25 16:07:33 -05:00
6699b52184 Merge pull request #758 from mailpoet/return_path_implementation
Implements return path for SMTP/PHPMail/AmazonSES [MAILPOET-761]
2016-12-23 19:32:08 +03:00
c651a8bbe8 - Implements return path for SMTP/PHPMail/AmazonSES 2016-12-23 11:09:10 -05:00
4a171dca2d Merge pull request #757 from mailpoet/date_fix
Fixes date formatting [MAILPOET-759]
2016-12-23 15:20:10 +03:00
f821a60a2c - Removes open tracking code URL when newsletter is previewed 2016-12-22 22:58:43 -05:00
24a3866e2a Merge pull request #755 from mailpoet/svn_publish_improve
Improve SVN publish command [MAILPOET-758]
2016-12-22 21:14:34 -05:00
fc54f31d3d - Prevents viewing newsletters if subscriber does not exist and token
does not match
2016-12-22 21:13:21 -05:00
616883ed63 - Fixes date formatting 2016-12-22 15:29:00 -05:00
b6ce513927 Save newsletter before sending preview [MAILPOET-702] 2016-12-22 21:56:34 +03:00
456152b5cb Merge pull request #753 from mailpoet/bounced_option_on_subscription_page
Display 'bounced' option on subscription page only if the user is bounced…
2016-12-22 09:34:00 -05:00
0be790971a Merge pull request #754 from mailpoet/utf8_fix
Removes setting DB character set to utf8 [MAILPOET-757]
2016-12-22 10:27:03 +03:00
e255484bc8 Quote path in awk command for Windows compatibility [MAILPOET-758] 2016-12-22 10:16:15 +03:00
fc53aca31d Remove database dependency for SVN publish task [MAILPOET-758] 2016-12-22 10:04:28 +03:00
31116a7cf6 - Removes setting DB character set to utf8 2016-12-21 18:21:36 -05:00
ee0c824126 Add form select block unit test 2016-12-21 21:22:52 +03:00
9ee66160ec Display 'bounced' option on subscription page only if user is bounced and make it disabled [MAILPOET-754] 2016-12-21 21:04:28 +03:00
bc91b12cf3 Bump up release version to 3.0.0-beta.9 2016-12-20 19:39:15 +03:00
457c43cd77 Merge pull request #752 from mailpoet/mysql_fix
MySQL configuration fix [MAILPOET-755]
2016-12-20 19:14:52 +03:00
949d6033d7 - Fixes DB configuration being partially set 2016-12-20 10:14:04 -05:00
cad6391fc6 - Updates the format of db timezone offset to two-digit hours and minutes 2016-12-20 10:13:11 -05:00
e5e5e7b426 - Fixes preview of unsent post notifications 2016-12-19 19:19:51 -05:00
9095482af2 - Updates unit tests 2016-12-18 23:24:58 -05:00
9698cf2d2e - Optimizes ViewInBrowser router endpoint
- Optimizes ViewInBrowser class
- Optimizes and updates shortcode link category to use the refactored
  getViewInBrowserUrl() method
- Updates Shortcodes to use the refactored getViewInBrowserUrl() method
2016-12-18 23:24:57 -05:00
707afc2ae0 - Adds a new method to create a URL data object as a numeric array instead
of associative, thus reducing the size of the object
- Adds a new method to convert numeric URL data object array into associative array
- Preserves backward compatibility with previous MP3 Beta versions by checking if the
  URL data object is already an associative array
- Adds different types of newsletter display
2016-12-18 23:24:50 -05:00
3b795a3e58 - Prevents deleted newsletters from showing up in archives
- Adds a relationship to the sending queue table
- Resets hash on newsletter duplication and notification history creation
- Updates hash generation to use random string instead of newsletter id
2016-12-18 23:08:08 -05:00
062f849fc8 - Isolates shortcodes regex into a class method
- Adds a new method to create a URL data object as a numeric array instead
  of associative, thus reducing the size of the object
- Adds a new method to convert numeric URL data object array into associative
  array
- Preserves backward compatibility with previous MP3 Beta versions
  by checking if the URL data object is already an associative array
2016-12-18 23:08:08 -05:00
98c6c29716 - Limits the length of subscriber token to allow for smaller URL data
objects
2016-12-18 23:08:07 -05:00
b4da3ecfb3 - Updates post processing filter naming convention
- Allows returning of text or html rendered body from the renderer
2016-12-18 23:08:07 -05:00
709f45941a - Allows returning of text or html rendered body from the sending queue
model
2016-12-18 23:08:07 -05:00
9ac4c3de72 - Adds new "hash" column to the newsletters table
- Updates newsletter model to automatically generate hash when saving
  newsletter
- Adds new getByHash method to the newsletter model
2016-12-18 23:08:07 -05:00
eee22227b3 - Removes unused class import
- Fixes newsletter URL generation in archive shortcode
- Disables generation of subscription management/unsubscribe/view in
  browser shortcodes when newsletter is previewed
2016-12-18 23:08:07 -05:00
edcce542c3 Merge pull request #747 from mailpoet/svn_push_command
Add a command to push new release to WP SVN [MAILPOET-673]
2016-12-16 14:50:37 +02:00
a354a380ba Merge pull request #749 from mailpoet/change_dup_import_msg
Change no subscribers added/updated string [MAILPOET-741]
2016-12-16 07:46:49 -05:00
54eb667654 Merge pull request #751 from mailpoet/makepot
Makepot task fix [MAILPOET-742]
2016-12-16 15:24:58 +03:00
10207112bc Fix regex matching __() function 2016-12-16 12:23:06 +02:00
892eea238f Merge pull request #750 from mailpoet/fix_bounce_api_response_code
Change bounce API OK response code from 201 to 200 [MAILPOET-747]
2016-12-15 17:24:09 -05:00
e6bb1666ee Remove obsolete wysija-newsletters gettext domain 2016-12-15 20:24:04 +02:00
2be9985d20 Fix finding __() and _n() i18n function calls 2016-12-15 20:24:04 +02:00
f4b7acca1e Fix parsing function call arguments when arg strings contain commas 2016-12-15 20:24:04 +02:00
de9d3655f0 Change bounce API OK response code from 201 to 200 [MAILPOET-747] 2016-12-15 19:49:39 +03:00
dfa13726e7 Add force mode to svn:publish command, add svn:checkout command 2016-12-15 16:20:12 +03:00
61ab583030 Merge pull request #748 from mailpoet/bump_wp_tested_to_4.7
Bump 'Tested up to' and 'Requires at least' versions [MAILPOET-744]
2016-12-15 12:20:19 +02:00
6954501915 Change no subscribers added/updated string [MAILPOET-741] 2016-12-15 12:02:06 +03:00
45a8103322 Bump 'Tested up to' and 'Requires at least' versions [MAILPOET-744] 2016-12-15 11:52:21 +03:00
c4896f4662 Merge pull request #746 from mailpoet/ALC_title_alignment
Fixes alignment not working for ALC titles [MAILPOET-734]
2016-12-15 10:59:23 +03:00
71711b4a0d Add a command to push new release to WP SVN 2016-12-14 18:24:41 +03:00
2634b606f6 - Fixes ALC transformer class to append style tags with semicolon 2016-12-13 19:32:50 -05:00
0aa48b9121 Bump up release version to 3.0.0-beta.8 2016-12-13 14:28:36 +02:00
1157cc8b9a Merge pull request #741 from mailpoet/ci_email_tests
Enables email sending tests in CI [MAILPOET-681]
2016-12-13 14:53:47 +03:00
88599963e0 Merge pull request #743 from mailpoet/vendor_conflict
Add dependency checking requirement [MAILPOET-690]
2016-12-12 15:24:56 -05:00
57706dc1b3 Merge pull request #745 from mailpoet/premium_plugin
Add action to notify 3rd party plugins that MP is initialized
2016-12-12 15:59:28 +02:00
694402e9f2 Merge pull request #744 from mailpoet/changelog_from_readme_on_update_page
Display changelog from readme.txt on Update page [MAILPOET-708]
2016-12-12 14:39:24 +02:00
891239bf4e Check readme file is readable before trying to parse it 2016-12-12 15:31:10 +03:00
232494e1a2 added 'mailpoet_initialized' action so that 3rd party plugin can check if mp is loaded 2016-12-12 11:28:38 +01:00
4ae55230da Display changelog from readme.txt on Update page [MAILPOET-708] 2016-12-12 10:04:36 +03:00
eda4a9edcc Fix regex to work on windows and match only mailpoet folder as valid 2016-12-09 23:20:48 +02:00
5fb699fd5b Remove trailing slash to support searches in Windows 2016-12-09 23:07:55 +02:00
7c5e0212ad Fix calling a method on an undefined variable 2016-12-09 22:44:43 +02:00
44a77e097b Escaped path when used in regex pattern 2016-12-09 22:41:44 +02:00
22fd9e31f7 Fix variable name typo 2016-12-09 18:12:22 +02:00
bbe3d48ec1 Add dependency checking to prevent conflicts with other plugins
MAILPOET-690
2016-12-09 18:05:19 +02:00
449978d7c1 Merge pull request #742 from mailpoet/mysql_group_fix
fix sql errors with mysql 5.6 & ONLY_FULL_GROUP_BY mode [MAILPOET-739]
2016-12-09 09:14:22 -05:00
0535e1eaeb Merge pull request #737 from mailpoet/sending_service_bounce_sync
Add bounce synchronization with MailPoet Sending Service [MAILPOET-696]
2016-12-08 21:47:55 -05:00
4dfe4e4997 fix sql errors with mysql 5.6 & ONLY_FULL_GROUP_BY mode 2016-12-08 18:38:03 +01:00
33a184fc4a Store emails sent during tests in test reports as an artifact 2016-12-08 18:19:50 +02:00
19b34ed838 Add a fake sendmail mailer to enable sending tests in CI 2016-12-08 17:57:21 +02:00
ca17e0c4da Merge pull request #740 from mailpoet/blockquote_rendering_fix
Fixes rendering of non-paragraph elements inside blockquote [MAILPOET-736]
2016-12-08 13:07:57 +02:00
347e491865 Merge pull request #738 from mailpoet/asset_cache_breaker
Add cache breaker query string to plugin static asset URLs [MAILPOET-713]
2016-12-08 10:51:02 +01:00
59b6877675 - Fixes non-paragraph elements inside blockquotes note being rendered
- Updates blockquote table width to 100% and allows text alignment
- Updates unit tests
2016-12-07 20:50:07 -05:00
6728203672 Add cache breaker query string to plugin static asset URLs
MAILPOET-713
2016-12-07 16:22:53 +02:00
8a1450d7d6 Add Mock API class to unit tests 2016-12-07 10:51:38 +03:00
49f2b147be Add fixes for MySQL strict mode 2016-12-07 10:31:38 +03:00
8f3b2e6c0b Fix code style 2016-12-06 23:06:45 +03:00
baf0d374ae Add bounce synchronization with MailPoet Sending Service [MAILPOET-696] 2016-12-06 22:48:36 +03:00
78 changed files with 2105 additions and 333 deletions

8
.circle_ci/fake-sendmail.rb Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/ruby
path = "/tmp"
Dir.mkdir(path) if !File.exists?(path)
File.open("#{path}/mailpoet-#{Time.now.to_f}.txt", "w") do |f|
sleep 5
f.puts ARGV.inspect
$stdin.each_line { |line| f.puts line }
end

View File

@ -0,0 +1,3 @@
; For Unix only. You may supply arguments as well (default: "sendmail -t -i").
; http://php.net/sendmail-path
sendmail_path = /home/ubuntu/mailpoet/.circle_ci/fake-sendmail.rb

View File

@ -13,4 +13,6 @@ WP_TEST_MAILER_MAILPOET_API=""
WP_TEST_MAILER_SENDGRID_API=""
WP_TEST_MAILER_SMTP_HOST=""
WP_TEST_MAILER_SMTP_LOGIN=""
WP_TEST_MAILER_SMTP_PASSWORD=""
WP_TEST_MAILER_SMTP_PASSWORD=""
WP_SVN_USERNAME=""
WP_SVN_PASSWORD=""

3
.gitignore vendored
View File

@ -17,4 +17,5 @@ tests/javascript/testBundles
assets/css/*.css
assets/js/*.js
.vagrant
lang
lang
.mp_svn

View File

@ -128,3 +128,20 @@ _n()
- Handlebars.
You can use Twig i18n functions in Handlebars, just load your template from a Twig view.
# Publish
Before you run a publishing command, you need to:
1. Ensure there is an up-to-date local copy of MailPoet SVN repository in `.mp_svn` directory by running `./do svn:checkout`.
2. Have all your features merged in Git `master`, your `mailpoet.php` and `readme.txt` tagged with a new version.
3. Run `./build.sh` to produce a `mailpoet.zip` distributable archive.
Everything's ready? Then run `./do svn:publish`.
If the job goes fine, you'll get a message like this:
```
Go to '.mp_svn' and run 'svn ci -m "Release 3.0.0-beta.9"' to publish the
release
```
It's quite literal: you can review the changes to be pushed and if you're satisfied, run the suggested command to finish the release publishing process.
If you're confident, execute `./do svn:publish --force` and your release will be published to the remote SVN repository without manual intervention (automatically). For easier authentication you might want to set `WP_SVN_USERNAME` and `WP_SVN_PASSWORD` environment variables.

View File

@ -99,15 +99,8 @@ class RoboFile extends \Robo\Tasks {
function makepot() {
return $this->_exec('./node_modules/.bin/grunt makepot'.
' --gruntfile '.__DIR__.'/tasks/makepot/makepot.js'.
' --base_path '.__DIR__
);
}
function pushpot() {
return $this->_exec('./node_modules/.bin/grunt pushpot'.
' --gruntfile '.__DIR__.'/tasks/makepot/makepot.js'.
' --base_path '.__DIR__
' --gruntfile='.__DIR__.'/tasks/makepot/makepot.js'.
' --base_path='.__DIR__
);
}
@ -199,8 +192,120 @@ class RoboFile extends \Robo\Tasks {
);
}
function svnCheckout() {
return $this->_exec('svn co https://plugins.svn.wordpress.org/mailpoet/ .mp_svn');
}
function svnPublish($opts = ['force' => false]) {
$this->loadWPFunctions();
$svn_dir = ".mp_svn";
$plugin_data = get_plugin_data('mailpoet.php', false, false);
$plugin_version = $plugin_data['Version'];
$plugin_dist_name = sanitize_title_with_dashes($plugin_data['Name']);
$plugin_dist_file = $plugin_dist_name . '.zip';
$this->say('Publishing version: ' . $plugin_version);
// Sanity checks
if(!is_readable($plugin_dist_file)) {
$this->say("Failed to access " . $plugin_dist_file);
return;
} elseif(!file_exists($svn_dir . "/.svn/")) {
$this->say("$svn_dir/.svn/ dir not found, is it a SVN repository?");
return;
} elseif(file_exists($svn_dir . "/tags/" . $plugin_version)) {
$this->say("A SVN tag already exists: " . $plugin_version);
return;
}
$collection = $this->collection();
// Clean up tmp dirs if the previous run was halted
if(file_exists("$svn_dir/trunk_new") || file_exists("$svn_dir/trunk_old")) {
$this->taskFileSystemStack()
->stopOnFail()
->remove(array("$svn_dir/trunk_new", "$svn_dir/trunk_old"))
->addToCollection($collection);
}
// Extract the distributable zip to tmp trunk dir
$this->taskExtract($plugin_dist_file)
->to("$svn_dir/trunk_new")
->preserveTopDirectory(false)
->addToCollection($collection);
// Rename current trunk
if(file_exists("$svn_dir/trunk")) {
$this->taskFileSystemStack()
->rename("$svn_dir/trunk", "$svn_dir/trunk_old")
->addToCollection($collection);
}
// Replace old trunk with a new one
$this->taskFileSystemStack()
->stopOnFail()
->rename("$svn_dir/trunk_new", "$svn_dir/trunk")
->remove("$svn_dir/trunk_old")
->addToCollection($collection);
// Windows compatibility
$awkCmd = '{print " --force \""$2"\""}';
// Mac OS X compatibility
$xargsFlag = (stripos(PHP_OS, 'Darwin') !== false) ? '' : '-r';
$this->taskExecStack()
->stopOnFail()
// Set SVN repo as working directory
->dir($svn_dir)
// Remove files from SVN repo that have already been removed locally
->exec("svn st | grep ^! | awk '$awkCmd' | xargs $xargsFlag svn rm")
// Recursively add files to SVN that haven't been added yet
->exec("svn add --force * --auto-props --parents --depth infinity -q")
// Tag the release
->exec("svn cp trunk tags/$plugin_version")
->addToCollection($collection);
$result = $collection->run();
if($result->wasSuccessful()) {
// Run or suggest release command depending on a flag
$release_cmd = "svn ci -m \"Release $plugin_version\"";
if(!empty($opts['force'])) {
$svn_login = getenv('WP_SVN_USERNAME');
$svn_password = getenv('WP_SVN_PASSWORD');
if ($svn_login && $svn_password) {
$release_cmd .= " --username $svn_login --password $svn_password";
} else {
$release_cmd .= ' --force-interactive';
}
$result = $this->taskExecStack()
->stopOnFail()
->dir($svn_dir)
->exec($release_cmd)
->run();
} else {
$this->yell(
"Go to '$svn_dir' and run '$release_cmd' to publish the release"
);
}
}
return $result;
}
protected function loadEnv() {
$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();
}
protected function loadWPFunctions() {
$this->loadEnv();
define('ABSPATH', getenv('WP_TEST_PATH') . '/');
define('WPINC', 'wp-includes');
require_once(ABSPATH . WPINC . '/functions.php');
require_once(ABSPATH . WPINC . '/formatting.php');
require_once(ABSPATH . WPINC . '/plugin.php');
require_once(ABSPATH . 'wp-admin/includes/plugin.php');
}
}

View File

@ -143,9 +143,7 @@ define('date',
var convertedFormat = [];
var escapeToken = false;
for (var index in format) {
var token = format[index];
for(var index = 0, token = ''; token = format.charAt(index); index++){
if (escapeToken === true) {
convertedFormat.push('['+token+']');
escapeToken = false;

View File

@ -40,7 +40,7 @@ define([
App.getChannel().trigger('beforeEditorSave', json);
// save newsletter
CommunicationComponent.saveNewsletter(json).done(function(response) {
return CommunicationComponent.saveNewsletter(json).done(function(response) {
if(response.success !== undefined && response.success === true) {
// TODO: Handle translations
//MailPoet.Notice.success("<?php _e('Newsletter has been saved.'); ?>");
@ -66,6 +66,14 @@ define([
});
};
// For getting a promise after triggering save event
Module.saveAndProvidePromise = function(saveResult) {
var promise = Module.save();
if (saveResult !== undefined) {
saveResult.promise = promise;
}
};
Module.getThumbnail = function(element, options) {
var promise = html2canvas(element, options || {});
@ -335,12 +343,12 @@ define([
};
App.on('before:start', function(options) {
App.save = Module.save;
App.save = Module.saveAndProvidePromise;
App.getChannel().on('autoSave', Module.autoSave);
window.onbeforeunload = Module.beforeExitWithUnsavedChanges;
App.getChannel().on('save', function() { App.save(); });
App.getChannel().on('save', function(saveResult) { App.save(saveResult); });
});
App.on('start', function(options) {

View File

@ -301,19 +301,25 @@ define([
// send test email
MailPoet.Modal.loading(true);
CommunicationComponent.previewNewsletter(data).always(function() {
MailPoet.Modal.loading(false);
}).done(function(response) {
MailPoet.Notice.success(
MailPoet.I18n.t('newsletterPreviewSent'),
{ scroll: true });
}).fail(function(response) {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }),
{ scroll: true, static: true }
);
}
// save before sending
var saveResult = {promise: null};
App.getChannel().trigger('save', saveResult);
saveResult.promise.always(function() {
CommunicationComponent.previewNewsletter(data).always(function() {
MailPoet.Modal.loading(false);
}).done(function(response) {
MailPoet.Notice.success(
MailPoet.I18n.t('newsletterPreviewSent'),
{ scroll: true });
}).fail(function(response) {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }),
{ scroll: true, static: true }
);
}
});
});
},
});

View File

@ -11,6 +11,8 @@ dependencies:
# install PHP dependencies for WordPress
- sudo apt-get update
- sudo apt-get --assume-yes install php5-mysql
# Add a fake sendmail mailer
- cp ./.circle_ci/mailpoet_php.ini /opt/circleci/php/$(phpenv global)/etc/conf.d/
# configure Apache
- sudo cp ./.circle_ci/apache/mailpoet.loc.conf /etc/apache2/sites-available
- sudo a2ensite mailpoet.loc
@ -64,3 +66,6 @@ test:
- cp tests/_output/report.xml $CIRCLE_TEST_REPORTS/codeception/report.xml
# Uncomment to copy PHP coverage report
#- cp tests/_output/coverage.xml $CIRCLE_TEST_REPORTS/codeception/coverage.xml
# Store any email output, sent via sendmail during tests
- mkdir $CIRCLE_TEST_REPORTS/fake-mailer
- cp /tmp/mailpoet-* $CIRCLE_TEST_REPORTS/fake-mailer

View File

@ -220,7 +220,9 @@ class Newsletters extends APIEndpoint {
$newsletter->save();
$subscriber = Subscriber::getCurrentWPUser();
$preview_url = NewsletterUrl::getViewInBrowserUrl(
$data, $subscriber, $queue = false, $preview = true
NewsletterUrl::TYPE_LISTING_EDITOR,
$newsletter,
$subscriber
);
return $this->successResponse(
@ -310,7 +312,7 @@ class Newsletters extends APIEndpoint {
if($newsletter->type === Newsletter::TYPE_STANDARD) {
$newsletter
->withSegments()
->withSegments(true)
->withSendingQueue()
->withStatistics();
} else if($newsletter->type === Newsletter::TYPE_WELCOME) {
@ -321,11 +323,11 @@ class Newsletters extends APIEndpoint {
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
$newsletter
->withOptions()
->withSegments()
->withSegments(true)
->withChildrenCount();
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION_HISTORY) {
$newsletter
->withSegments()
->withSegments(true)
->withSendingQueue()
->withStatistics();
}
@ -339,7 +341,11 @@ class Newsletters extends APIEndpoint {
// get preview url
$subscriber = Subscriber::getCurrentWPUser();
$newsletter->preview_url = NewsletterUrl::getViewInBrowserUrl(
$newsletter, $subscriber, $queue, $preview = true);
NewsletterUrl::TYPE_LISTING_EDITOR,
$newsletter,
$subscriber,
$queue
);
$data[] = $newsletter->asArray();
}

View File

@ -29,6 +29,7 @@ class Env {
static $db_password;
static $db_charset;
static $db_timezone_offset;
static $required_permission = 'manage_options';
static function init($file, $version) {
global $wpdb;
@ -80,12 +81,13 @@ class Env {
return implode('', $source_name);
}
private static function getDbTimezoneOffset() {
$mins = get_option('gmt_offset') * 60;
static function getDbTimezoneOffset($offset = false) {
$offset = ($offset) ? $offset : get_option('gmt_offset');
$mins = $offset * 60;
$sgn = ($mins < 0 ? -1 : 1);
$mins = abs($mins);
$hrs = floor($mins / 60);
$mins -= $hrs * 60;
return sprintf('%+d:%02d', $hrs * $sgn, $mins);
return sprintf('%+03d:%02d', $hrs * $sgn, $mins);
}
}

View File

@ -22,10 +22,11 @@ class Initializer {
}
function init() {
$requiments_check_results = $this->checkRequirements();
$requirements_check_results = $this->checkRequirements();
// abort initialization if PDO extension is missing
if(!$requiments_check_results[RequirementsChecker::TEST_PDO_EXTENSION]) return;
if(!$requirements_check_results[RequirementsChecker::TEST_PDO_EXTENSION] ||
!$requirements_check_results[RequirementsChecker::TEST_VENDOR_SOURCE]) return;
$this->setupDB();
@ -51,9 +52,9 @@ class Initializer {
\ORM::configure('password', Env::$db_password);
\ORM::configure('logging', WP_DEBUG);
\ORM::configure('driver_options', array(
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
\PDO::MYSQL_ATTR_INIT_COMMAND =>
'SET TIME_ZONE = "' . Env::$db_timezone_offset. '"'
'SET TIME_ZONE = "' . Env::$db_timezone_offset . '", ' .
'sql_mode=(SELECT REPLACE(@@sql_mode,"ONLY_FULL_GROUP_BY",""))'
));
$settings = Env::$db_prefix . 'settings';
@ -128,6 +129,7 @@ class Initializer {
$this->setupAPI();
$this->setupRouter();
$this->setupPages();
do_action('mailpoet_initialized', MAILPOET_VERSION);
} catch(\Exception $e) {
$this->handleFailedInitialization($e);
}

View File

@ -17,6 +17,7 @@ use MailPoet\Listing;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\DateTime;
use MailPoet\WP\Notice as WPNotice;
use MailPoet\WP\Readme;
if(!defined('ABSPATH')) exit;
@ -44,7 +45,7 @@ class Menu {
add_menu_page(
'MailPoet',
'MailPoet',
'manage_options',
Env::$required_permission,
$main_page_slug,
null,
$this->assets_url . '/img/menu_icon.png',
@ -55,7 +56,7 @@ class Menu {
$main_page_slug,
$this->setPageTitle(__('Emails', 'mailpoet')),
__('Emails', 'mailpoet'),
'manage_options',
Env::$required_permission,
$main_page_slug,
array($this, 'newsletters')
);
@ -75,7 +76,7 @@ class Menu {
$main_page_slug,
$this->setPageTitle(__('Forms', 'mailpoet')),
__('Forms', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-forms',
array($this, 'forms')
);
@ -94,7 +95,7 @@ class Menu {
$main_page_slug,
$this->setPageTitle(__('Subscribers', 'mailpoet')),
__('Subscribers', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-subscribers',
array($this, 'subscribers')
);
@ -113,7 +114,7 @@ class Menu {
$main_page_slug,
$this->setPageTitle(__('Lists', 'mailpoet')),
__('Lists', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-segments',
array($this, 'segments')
);
@ -133,7 +134,7 @@ class Menu {
$main_page_slug,
$this->setPageTitle( __('Settings', 'mailpoet')),
__('Settings', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-settings',
array($this, 'settings')
);
@ -141,7 +142,7 @@ class Menu {
'admin.php?page=mailpoet-subscribers',
$this->setPageTitle( __('Import', 'mailpoet')),
__('Import', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-import',
array($this, 'import')
);
@ -150,7 +151,7 @@ class Menu {
true,
$this->setPageTitle(__('Export', 'mailpoet')),
__('Export', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-export',
array($this, 'export')
);
@ -159,7 +160,7 @@ class Menu {
true,
$this->setPageTitle(__('Welcome', 'mailpoet')),
__('Welcome', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-welcome',
array($this, 'welcome')
);
@ -168,7 +169,7 @@ class Menu {
true,
$this->setPageTitle(__('Update', 'mailpoet')),
__('Update', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-update',
array($this, 'update')
);
@ -177,7 +178,7 @@ class Menu {
true,
$this->setPageTitle(__('Form Editor', 'mailpoet')),
__('Form Editor', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-form-editor',
array($this, 'formEditor')
);
@ -186,7 +187,7 @@ class Menu {
true,
$this->setPageTitle(__('Newsletter', 'mailpoet')),
__('Newsletter Editor', 'mailpoet'),
'manage_options',
Env::$required_permission,
'mailpoet-newsletter-editor',
array($this, 'newletterEditor')
);
@ -242,6 +243,14 @@ class Menu {
'sub_menu' => 'mailpoet-newsletters'
);
$readme_file = Env::$path . '/readme.txt';
if(is_readable($readme_file)) {
$changelog = Readme::parseChangelog(file_get_contents($readme_file), 2);
if($changelog) {
$data['changelog'] = $changelog;
}
}
$this->displayPage('update.html', $data);
}

View File

@ -106,6 +106,7 @@ class Migrator {
function sendingQueues() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'type varchar(12) NULL DEFAULT NULL,',
'newsletter_id mediumint(9) NOT NULL,',
'newsletter_rendered_body longtext,',
'newsletter_rendered_subject varchar(250) NULL DEFAULT NULL,',
@ -176,6 +177,7 @@ class Migrator {
function newsletters() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'hash varchar(150) NULL DEFAULT NULL,',
'parent_id mediumint(9) NULL,',
'subject varchar(250) NOT NULL DEFAULT "",',
'type varchar(20) NOT NULL DEFAULT "standard",',

View File

@ -55,8 +55,8 @@ class Renderer {
function setupGlobalVariables() {
$this->renderer->addExtension(new Twig\Assets(array(
'assets_url' => Env::$assets_url,
'assets_path' => Env::$assets_path
'version' => Env::$version,
'assets_url' => Env::$assets_url
)));
}

View File

@ -1,6 +1,7 @@
<?php
namespace MailPoet\Config;
use MailPoet\Config\Env;
use MailPoet\Util\Helpers;
use MailPoet\WP\Notice as WPNotice;
@ -11,7 +12,30 @@ class RequirementsChecker {
const TEST_FOLDER_PERMISSIONS = 'TempAndCacheFolderCreation';
const TEST_PDO_EXTENSION = 'PDOExtension';
const TEST_MBSTRING_EXTENSION = 'MbstringExtension';
const TEST_VENDOR_SOURCE = 'VendorSource';
public $display_error_notice;
public $vendor_classes = array(
'\ORM',
'\Model',
'\Twig_Environment',
'\Twig_Loader_Filesystem',
'\Twig_Lexer',
'\Twig_Extension',
'\Twig_Extension_GlobalsInterface',
'\Twig_SimpleFunction',
'\Swift_Mailer',
'\Swift_SmtpTransport',
'\Swift_Message',
'\Carbon\Carbon',
'\Sudzy\ValidModel',
'\Sudzy\ValidationException',
'\Sudzy\Engine',
'\pQuery',
'\Cron\CronExpression',
'\Html2Text\Html2Text',
'\csstidy'
);
function __construct($display_error_notice = true) {
$this->display_error_notice = $display_error_notice;
@ -22,7 +46,8 @@ class RequirementsChecker {
self::TEST_PDO_EXTENSION,
self::TEST_PHP_VERSION,
self::TEST_FOLDER_PERMISSIONS,
self::TEST_MBSTRING_EXTENSION
self::TEST_MBSTRING_EXTENSION,
self::TEST_VENDOR_SOURCE
);
$results = array();
foreach($available_tests as $test) {
@ -84,10 +109,47 @@ class RequirementsChecker {
return true;
}
function checkVendorSource() {
foreach($this->vendor_classes as $dependency) {
$dependency_path = $this->getDependencyPath($dependency);
if(!$dependency_path) {
$error = sprintf(
__('A MailPoet dependency (%s) does not appear to be loaded correctly, thus MailPoet will not work correctly. Please reinstall the plugin.', 'mailpoet'),
$dependency
);
return $this->processError($error);
}
$pattern = '#' . preg_quote(Env::$path) . '[\\\/]#';
$is_loaded_by_plugin = preg_match($pattern, $dependency_path);
if(!$is_loaded_by_plugin) {
$error = sprintf(
__('MailPoet has detected a dependency conflict (%s) with another plugin (%s), which may cause unexpected behavior. Please disable the offending plugin to fix this issue.', 'mailpoet'),
$dependency,
$dependency_path
);
return $this->processError($error);
}
}
return true;
}
private function getDependencyPath($namespaced_class) {
try {
$reflector = new \ReflectionClass($namespaced_class);
return $reflector->getFileName();
} catch(\ReflectionException $ex) {
return false;
}
}
function processError($error) {
if($this->display_error_notice) {
WPNotice::displayError($error);
}
return false;
}
}
}

View File

@ -3,7 +3,6 @@ namespace MailPoet\Config;
use \MailPoet\Models\Newsletter;
use \MailPoet\Models\Subscriber;
use \MailPoet\Models\SubscriberSegment;
use \MailPoet\Subscription;
use MailPoet\Newsletter\Url as NewsletterUrl;
class Shortcodes {
@ -33,7 +32,7 @@ class Shortcodes {
), 2);
add_filter('mailpoet_archive_subject', array(
$this, 'renderArchiveSubject'
), 2);
), 2, 3);
}
function formWidget($params = array()) {
@ -78,6 +77,8 @@ class Shortcodes {
$newsletters = Newsletter::getArchives($segment_ids);
$subscriber = Subscriber::getCurrentWPUser();
if(empty($newsletters)) {
return apply_filters(
'mailpoet_archive_no_newsletters',
@ -91,12 +92,13 @@ class Shortcodes {
$html .= '<ul class="mailpoet_archive">';
foreach($newsletters as $newsletter) {
$queue = $newsletter->queue()->findOne();
$html .= '<li>'.
'<span class="mailpoet_archive_date">'.
apply_filters('mailpoet_archive_date', $newsletter).
'</span>
<span class="mailpoet_archive_subject">'.
apply_filters('mailpoet_archive_subject', $newsletter).
apply_filters('mailpoet_archive_subject', $newsletter, $subscriber, $queue).
'</span>
</li>';
}
@ -112,13 +114,16 @@ class Shortcodes {
);
}
function renderArchiveSubject($newsletter) {
$preview_url = NewsletterUrl::getViewInBrowserUrl($newsletter);
function renderArchiveSubject($newsletter, $subscriber, $queue) {
$preview_url = NewsletterUrl::getViewInBrowserUrl(
NewsletterUrl::TYPE_ARCHIVE,
$newsletter,
$subscriber,
$queue
);
return '<a href="'.esc_attr($preview_url).'" target="_blank" title="'
.esc_attr(__('Preview in a new tab', 'mailpoet')).'">'
.esc_attr($newsletter->subject).
'</a>';
}
}
}

View File

@ -2,6 +2,7 @@
namespace MailPoet\Cron;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
if(!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-includes/pluggable.php');
@ -43,6 +44,7 @@ class Daemon {
try {
$this->executeScheduleWorker();
$this->executeQueueWorker();
$this->executeBounceWorker();
} catch(\Exception $e) {
// continue processing, no need to handle errors
}
@ -74,6 +76,11 @@ class Daemon {
return $queue->process();
}
function executeBounceWorker() {
$bounce = new BounceWorker($this->timer);
return $bounce->process();
}
function callSelf() {
CronHelper::accessDaemon($this->token, self::REQUEST_TIMEOUT);
return $this->terminateRequest();

View File

@ -4,6 +4,7 @@ namespace MailPoet\Cron\Triggers;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
use MailPoet\Mailer\MailerLog;
if(!defined('ABSPATH')) exit;
@ -19,7 +20,11 @@ class WordPress {
$scheduled_queues = SchedulerWorker::getScheduledQueues();
$running_queues = SendingQueueWorker::getRunningQueues();
$sending_limit_reached = MailerLog::isSendingLimitReached();
return (($scheduled_queues || $running_queues) && !$sending_limit_reached);
$bounce_sync_available = BounceWorker::checkBounceSyncAvailable();
$bounce_due_queues = BounceWorker::getAllDueQueues();
$bounce_future_queues = BounceWorker::getFutureQueues();
return (($scheduled_queues || $running_queues) && !$sending_limit_reached)
|| ($bounce_sync_available && ($bounce_due_queues || !$bounce_future_queues));
}
static function cleanup() {

197
lib/Cron/Workers/Bounce.php Normal file
View File

@ -0,0 +1,197 @@
<?php
namespace MailPoet\Cron\Workers;
use Carbon\Carbon;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Subscriber;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class Bounce {
const BOUNCED_HARD = 'hard';
const BOUNCED_SOFT = 'soft';
const NOT_BOUNCED = null;
const BATCH_SIZE = 100;
public $timer;
public $api;
function __construct($timer = false) {
$this->timer = ($timer) ? $timer : microtime(true);
// abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
}
static function checkBounceSyncAvailable() {
$mailer_config = Mailer::getMailerConfig();
return !empty($mailer_config['method'])
&& $mailer_config['method'] === Mailer::METHOD_MAILPOET;
}
function initApi() {
if(!$this->api) {
$mailer_config = Mailer::getMailerConfig();
$this->api = new Bounce\API($mailer_config['mailpoet_api_key']);
}
}
function process() {
if(!self::checkBounceSyncAvailable()) {
return false;
}
$this->initApi();
$scheduled_queues = self::getScheduledQueues();
$running_queues = self::getRunningQueues();
if(!$scheduled_queues && !$running_queues) {
self::scheduleBounceSync();
return false;
}
foreach($scheduled_queues as $i => $queue) {
$this->prepareBounceQueue($queue);
}
foreach($running_queues as $i => $queue) {
$this->processBounceQueue($queue);
}
return true;
}
static function scheduleBounceSync() {
$already_scheduled = SendingQueue::where('type', 'bounce')
->whereNull('deleted_at')
->where('status', SendingQueue::STATUS_SCHEDULED)
->findMany();
if($already_scheduled) {
return false;
}
$queue = SendingQueue::create();
$queue->type = 'bounce';
$queue->status = SendingQueue::STATUS_SCHEDULED;
$queue->priority = SendingQueue::PRIORITY_LOW;
$queue->scheduled_at = self::getNextRunDate();
$queue->newsletter_id = 0;
$queue->save();
return $queue;
}
function prepareBounceQueue(SendingQueue $queue) {
$subscribers = Subscriber::select('id')
->whereNull('deleted_at')
->whereIn('status', array(
Subscriber::STATUS_SUBSCRIBED,
Subscriber::STATUS_UNCONFIRMED
))
->findArray();
$subscribers = Helpers::arrayColumn($subscribers, 'id');
if(empty($subscribers)) {
$queue->delete();
return false;
}
// update current queue
$queue->subscribers = serialize(
array(
'to_process' => $subscribers
)
);
$queue->count_total = $queue->count_to_process = count($subscribers);
$queue->status = null;
$queue->save();
// abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
return true;
}
function processBounceQueue(SendingQueue $queue) {
$queue->subscribers = $queue->getSubscribers();
if(empty($queue->subscribers['to_process'])) {
$queue->delete();
return false;
}
$subscriber_batches = array_chunk(
$queue->subscribers['to_process'],
self::BATCH_SIZE
);
foreach($subscriber_batches as $subscribers_to_process_ids) {
// abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
$subscriber_emails = Subscriber::select('email')
->whereIn('id', $subscribers_to_process_ids)
->whereNull('deleted_at')
->findArray();
$subscriber_emails = Helpers::arrayColumn($subscriber_emails, 'email');
$this->processEmails($subscriber_emails);
$queue->updateProcessedSubscribers($subscribers_to_process_ids);
}
return true;
}
function processEmails(array $subscriber_emails) {
$checked_emails = $this->api->check($subscriber_emails);
$this->processApiResponse((array)$checked_emails);
}
function processApiResponse(array $checked_emails) {
foreach($checked_emails as $email) {
if(!isset($email['address'], $email['bounce'])) {
continue;
}
if($email['bounce'] === self::BOUNCED_HARD) {
$subscriber = Subscriber::findOne($email['address']);
$subscriber->status = Subscriber::STATUS_BOUNCED;
$subscriber->save();
}
}
}
static function getNextRunDate() {
$date = Carbon::createFromTimestamp(current_time('timestamp'));
// Random day of the next week
$date->setISODate($date->format('o'), $date->format('W') + 1, mt_rand(1, 7));
$date->startOfDay();
return $date;
}
static function getScheduledQueues($future = false) {
$dateWhere = ($future) ? 'whereGt' : 'whereLte';
return SendingQueue::where('type', 'bounce')
->$dateWhere('scheduled_at', Carbon::createFromTimestamp(current_time('timestamp')))
->whereNull('deleted_at')
->where('status', SendingQueue::STATUS_SCHEDULED)
->findMany();
}
static function getRunningQueues() {
return SendingQueue::where('type', 'bounce')
->whereLte('scheduled_at', Carbon::createFromTimestamp(current_time('timestamp')))
->whereNull('deleted_at')
->whereNull('status')
->findMany();
}
static function getAllDueQueues() {
$scheduled_queues = self::getScheduledQueues();
$running_queues = self::getRunningQueues();
return array_merge((array)$scheduled_queues, (array)$running_queues);
}
static function getFutureQueues() {
return self::getScheduledQueues(true);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace MailPoet\Cron\Workers\Bounce;
if(!defined('ABSPATH')) exit;
class API {
public $url = 'https://bridge.mailpoet.com/api/v0/bounces/search';
public $api_key;
function __construct($api_key) {
$this->api_key = $api_key;
}
function check(array $emails) {
$result = wp_remote_post(
$this->url,
$this->request($emails)
);
if(wp_remote_retrieve_response_code($result) === 200) {
return json_decode(wp_remote_retrieve_body($result), true);
}
return false;
}
private function auth() {
return 'Basic ' . base64_encode('api:' . $this->api_key);
}
private function request($body) {
return array(
'timeout' => 10,
'httpversion' => '1.0',
'method' => 'POST',
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => $this->auth()
),
'body' => json_encode($body)
);
}
}

View File

@ -187,6 +187,7 @@ class Scheduler {
static function getScheduledQueues() {
return SendingQueue::where('status', 'scheduled')
->whereLte('scheduled_at', Carbon::createFromTimestamp(current_time('timestamp')))
->whereNull('type')
->findMany();
}
}

View File

@ -167,6 +167,7 @@ class SendingQueue {
return SendingQueueModel::orderByAsc('priority')
->whereNull('deleted_at')
->whereNull('status')
->whereNull('type')
->findMany();
}
}

View File

@ -29,11 +29,17 @@ class Select extends Base {
);
foreach($options as $option) {
if(!empty($option['is_hidden'])) {
continue;
}
$is_selected = (
(isset($option['is_checked']) && $option['is_checked'])
||
(self::getFieldValue($block) === $option['value'])
) ? 'selected="selected"' : '';
) ? ' selected="selected"' : '';
$is_disabled = (!empty($option['is_disabled'])) ? ' disabled="disabled"' : '';
if(is_array($option['value'])) {
$value = key($option['value']);
@ -43,7 +49,7 @@ class Select extends Base {
$label = $option['value'];
}
$html .= '<option value="'.$value.'" '.$is_selected.'>';
$html .= '<option value="'.$value.'"' . $is_selected . $is_disabled . '>';
$html .= esc_attr($label);
$html .= '</option>';
}

View File

@ -10,6 +10,7 @@ class Mailer {
public $mailer_config;
public $sender;
public $reply_to;
public $return_path;
public $mailer_instance;
const MAILER_CONFIG_SETTING_NAME = 'mta';
const SENDING_LIMIT_INTERVAL_MULTIPLIER = 60;
@ -19,10 +20,11 @@ class Mailer {
const METHOD_PHPMAIL = 'PHPMail';
const METHOD_SMTP = 'SMTP';
function __construct($mailer = false, $sender = false, $reply_to = false) {
function __construct($mailer = false, $sender = false, $reply_to = false, $return_path = false) {
$this->mailer_config = self::getMailerConfig($mailer);
$this->sender = $this->getSenderNameAndAddress($sender);
$this->reply_to = $this->getReplyToNameAndAddress($reply_to);
$this->return_path = $this->getReturnPathAddress($return_path);
$this->mailer_instance = $this->buildMailer();
}
@ -39,7 +41,8 @@ class Mailer {
$this->mailer_config['access_key'],
$this->mailer_config['secret_key'],
$this->sender,
$this->reply_to
$this->reply_to,
$this->return_path
);
break;
case self::METHOD_MAILPOET:
@ -59,7 +62,8 @@ class Mailer {
case self::METHOD_PHPMAIL:
$mailer_instance = new $this->mailer_config['class'](
$this->sender,
$this->reply_to
$this->reply_to,
$this->return_path
);
break;
case self::METHOD_SMTP:
@ -71,7 +75,8 @@ class Mailer {
$this->mailer_config['password'],
$this->mailer_config['encryption'],
$this->sender,
$this->reply_to
$this->reply_to,
$this->return_path
);
break;
default:
@ -112,7 +117,7 @@ class Mailer {
function getReplyToNameAndAddress($reply_to = array()) {
if(!$reply_to) {
$reply_to = Setting::getValue('reply_to', null);
$reply_to = Setting::getValue('reply_to');
$reply_to['name'] = (!empty($reply_to['name'])) ?
$reply_to['name'] :
$this->sender['from_name'];
@ -131,6 +136,12 @@ class Mailer {
);
}
function getReturnPathAddress($return_path) {
return ($return_path) ?
$return_path :
Setting::getValue('bounce.address');
}
function formatSubscriberNameAndEmailAddress($subscriber) {
$subscriber = (is_object($subscriber)) ? $subscriber->asArray() : $subscriber;
if(!is_array($subscriber)) return $subscriber;

View File

@ -17,6 +17,7 @@ class AmazonSES {
public $url;
public $sender;
public $reply_to;
public $return_path;
public $date;
public $date_without_time;
private $available_regions = array(
@ -25,7 +26,7 @@ class AmazonSES {
'EU (Ireland)' => 'eu-west-1'
);
function __construct($region, $access_key, $secret_key, $sender, $reply_to) {
function __construct($region, $access_key, $secret_key, $sender, $reply_to, $return_path) {
$this->aws_access_key = $access_key;
$this->aws_secret_key = $secret_key;
$this->aws_region = (in_array($region, $this->available_regions)) ? $region : false;
@ -40,11 +41,13 @@ class AmazonSES {
$this->url = 'https://' . $this->aws_endpoint;
$this->sender = $sender;
$this->reply_to = $reply_to;
$this->return_path = ($return_path) ?
$return_path :
$this->sender['from_email'];
$this->date = gmdate('Ymd\THis\Z');
$this->date_without_time = gmdate('Ymd');
}
function send($newsletter, $subscriber) {
$result = wp_remote_post(
$this->url,
@ -71,7 +74,7 @@ class AmazonSES {
'Source' => $this->sender['from_name_email'],
'ReplyToAddresses.member.1' => $this->reply_to['reply_to_name_email'],
'Message.Subject.Data' => $newsletter['subject'],
'ReturnPath' => $this->sender['from_name_email'],
'ReturnPath' => $this->return_path
);
if(!empty($newsletter['body']['html'])) {
$body['Message.Body.Html.Data'] = $newsletter['body']['html'];

View File

@ -8,11 +8,15 @@ if(!defined('ABSPATH')) exit;
class PHPMail {
public $sender;
public $reply_to;
public $return_path;
public $mailer;
function __construct($sender, $reply_to) {
function __construct($sender, $reply_to, $return_path) {
$this->sender = $sender;
$this->reply_to = $reply_to;
$this->return_path = ($return_path) ?
$return_path :
$this->sender['from_email'];
$this->mailer = $this->buildMailer();
}
@ -44,6 +48,7 @@ class PHPMail {
->setReplyTo(array(
$this->reply_to['reply_to_email'] => $this->reply_to['reply_to_name']
))
->setReturnPath($this->return_path)
->setSubject($newsletter['subject']);
if(!empty($newsletter['body']['html'])) {
$message = $message->setBody($newsletter['body']['html'], 'text/html');

View File

@ -14,11 +14,12 @@ class SMTP {
public $encryption;
public $sender;
public $reply_to;
public $return_path;
public $mailer;
function __construct(
$host, $port, $authentication, $login = null, $password = null, $encryption,
$sender, $reply_to) {
$sender, $reply_to, $return_path) {
$this->host = $host;
$this->port = $port;
$this->authentication = $authentication;
@ -27,6 +28,9 @@ class SMTP {
$this->encryption = $encryption;
$this->sender = $sender;
$this->reply_to = $reply_to;
$this->return_path = ($return_path) ?
$return_path :
$this->sender['from_email'];
$this->mailer = $this->buildMailer();
}
@ -65,6 +69,7 @@ class SMTP {
->setReplyTo(array(
$this->reply_to['reply_to_email'] => $this->reply_to['reply_to_name']
))
->setReturnPath($this->return_path)
->setSubject($newsletter['subject']);
if(!empty($newsletter['body']['html'])) {
$message = $message->setBody($newsletter['body']['html'], 'text/html');

View File

@ -2,6 +2,7 @@
namespace MailPoet\Models;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security;
if(!defined('ABSPATH')) exit;
@ -11,7 +12,6 @@ class Newsletter extends Model {
const TYPE_WELCOME = 'welcome';
const TYPE_NOTIFICATION = 'notification';
const TYPE_NOTIFICATION_HISTORY = 'notification_history';
// standard newsletters
const STATUS_DRAFT = 'draft';
const STATUS_SCHEDULED = 'scheduled';
@ -19,6 +19,7 @@ class Newsletter extends Model {
const STATUS_SENT = 'sent';
// automatic newsletters status
const STATUS_ACTIVE = 'active';
const NEWSLETTER_HASH_LENGTH = 6;
function __construct() {
parent::__construct();
@ -27,6 +28,10 @@ class Newsletter extends Model {
));
}
function queue() {
return $this->has_one(__NAMESPACE__ . '\SendingQueue', 'newsletter_id', 'id');
}
function save() {
if(is_string($this->deleted_at) && strlen(trim($this->deleted_at)) === 0) {
$this->set_expr('deleted_at', 'NULL');
@ -37,6 +42,12 @@ class Newsletter extends Model {
? json_encode($this->body)
: $this->body
);
$this->set('hash',
($this->hash)
? $this->hash
: self::generateHash()
);
return parent::save();
}
@ -74,6 +85,9 @@ class Newsletter extends Model {
// reset status
$duplicate->set('status', self::STATUS_DRAFT);
// reset hash
$duplicate->set('hash', null);
$duplicate->save();
if($duplicate->getErrors() === false) {
@ -130,6 +144,9 @@ class Newsletter extends Model {
$notification_history->set_expr('updated_at', 'NOW()');
$notification_history->set_expr('deleted_at', 'NULL');
// reset hash
$notification_history->set('hash', null);
$notification_history->save();
if($notification_history->getErrors() === false) {
@ -182,8 +199,39 @@ class Newsletter extends Model {
);
}
function withSegments() {
function withSegments($incl_deleted = false) {
$this->segments = $this->segments()->findArray();
if($incl_deleted) {
$this->withDeletedSegments();
}
return $this;
}
// Intermediary table only
function segmentLinks() {
return $this->has_many(
__NAMESPACE__.'\NewsletterSegment',
'newsletter_id',
'id'
);
}
function withDeletedSegments() {
if(!empty($this->segments)) {
$segment_ids = Helpers::arrayColumn($this->segments, 'id');
$links = $this->segmentLinks()
->whereNotIn('segment_id', $segment_ids)->findArray();
$deleted_segments = array();
foreach($links as $link) {
$deleted_segments[] = array(
'id' => $link['segment_id'],
'name' => __('Deleted list', 'mailpoet')
);
}
$this->segments = array_merge($this->segments, $deleted_segments);
}
return $this;
}
@ -634,6 +682,7 @@ class Newsletter extends Model {
'queues'
)
->where('queues.status', SendingQueue::STATUS_COMPLETED)
->whereNull('newsletters.deleted_at')
->select('queues.processed_at')
->orderByDesc('queues.processed_at');
@ -647,4 +696,17 @@ class Newsletter extends Model {
}
return $orm->findMany();
}
}
static function getByHash($hash) {
return parent::where('hash', $hash)
->findOne();
}
static function generateHash() {
return substr(
md5(AUTH_KEY . Security::generateRandomString(15)),
0,
self::NEWSLETTER_HASH_LENGTH
);
}
}

View File

@ -70,10 +70,13 @@ class SendingQueue extends Model {
return $subscribers;
}
function getNewsletterRenderedBody() {
return (!is_serialized($this->newsletter_rendered_body)) ?
function getNewsletterRenderedBody($type = false) {
$rendered_newsletter = (!is_serialized($this->newsletter_rendered_body)) ?
$this->newsletter_rendered_body :
unserialize($this->newsletter_rendered_body);
return ($type && !empty($rendered_newsletter[$type])) ?
$rendered_newsletter[$type] :
$rendered_newsletter;
}
function isSubscriberProcessed($subscriber_id) {

View File

@ -14,8 +14,8 @@ class Subscriber extends Model {
const STATUS_UNSUBSCRIBED = 'unsubscribed';
const STATUS_UNCONFIRMED = 'unconfirmed';
const STATUS_BOUNCED = 'bounced';
const SUBSCRIPTION_LIMIT_COOLDOWN = 60;
const SUBSCRIBER_TOKEN_LENGTH = 6;
function __construct() {
parent::__construct();
@ -154,13 +154,17 @@ class Subscriber extends Model {
static function generateToken($email = null) {
if($email !== null) {
return md5(AUTH_KEY.$email);
return substr(md5(AUTH_KEY . $email), 0, self::SUBSCRIBER_TOKEN_LENGTH);
}
return false;
}
static function verifyToken($email, $token) {
return call_user_func('hash_equals', self::generateToken($email), $token);
return call_user_func(
'hash_equals',
self::generateToken($email),
substr($token, 0, self::SUBSCRIBER_TOKEN_LENGTH)
);
}
static function subscribe($subscriber_data = array(), $segment_ids = array()) {

View File

@ -169,6 +169,6 @@ class PostTransformer {
$alignment = (in_array($this->args['titleAlignment'], array('left', 'right', 'center'))) ? $this->args['titleAlignment'] : 'left';
return '<' . $tag . ' data-post-id="' . $post->ID . '" style="text-align: ' . $alignment . '">' . $title . '</' . $tag . '>';
return '<' . $tag . ' data-post-id="' . $post->ID . '" style="text-align: ' . $alignment . ';">' . $title . '</' . $tag . '>';
}
}

View File

@ -29,6 +29,6 @@ class TitleListTransformer {
$title = '<a href="' . get_permalink($post->ID) . '">' . $title . '</a>';
}
return '<li style="text-align: ' . $alignment . '">' . $title . '</li>';
return '<li style="text-align: ' . $alignment . ';">' . $title . '</li>';
}
}

View File

@ -6,6 +6,7 @@ use MailPoet\Router\Router;
use MailPoet\Router\Endpoints\Track as TrackEndpoint;
use MailPoet\Models\NewsletterLink;
use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security;
class Links {
@ -101,24 +102,19 @@ class Links {
$preview = false
) {
// match data tags
$regex = sprintf(
'/((%s|%s)(?:-\w+)?)/',
preg_quote(self::DATA_TAG_CLICK),
preg_quote(self::DATA_TAG_OPEN)
);
$subscriber = Subscriber::findOne($subscriber_id);
preg_match_all($regex, $content, $matches);
preg_match_all(self::getLinkRegex(), $content, $matches);
foreach($matches[1] as $index => $match) {
$hash = null;
if(preg_match('/-/', $match)) {
list(, $hash) = explode('-', $match);
}
$data = array(
'subscriber_id' => $subscriber->id,
'subscriber_token' => Subscriber::generateToken($subscriber->email),
'queue_id' => $queue_id,
'link_hash' => $hash,
'preview' => $preview
$data = self::createUrlDataObject(
$subscriber->id,
$subscriber->email,
$queue_id,
$hash,
$preview
);
$router_action = ($matches[2][$index] === self::DATA_TAG_CLICK) ?
TrackEndpoint::ACTION_CLICK :
@ -144,4 +140,54 @@ class Links {
$newsletter_link->save();
}
}
static function convertHashedLinksToShortcodesAndUrls($content, $convert_all = false) {
preg_match_all(self::getLinkRegex(), $content, $links);
$links = array_unique(Helpers::flattenArray($links));
foreach($links as $link) {
$link_hash = explode('-', $link);
if(!isset($link_hash[1])) continue;
$newsletter_link = NewsletterLink::getByHash($link_hash[1]);
// convert either only link shortcodes or all hashes links if "convert all"
// option is specified
if($newsletter_link &&
(preg_match('/\[link:/', $newsletter_link->url) || $convert_all)
) {
$content = str_replace($link, $newsletter_link->url, $content);
}
}
return $content;
}
static function getLinkRegex() {
return sprintf(
'/((%s|%s)(?:-\w+)?)/',
preg_quote(self::DATA_TAG_CLICK),
preg_quote(self::DATA_TAG_OPEN)
);
}
static function createUrlDataObject(
$subscriber_id, $subscriber_email, $queue_id, $link_hash, $preview
) {
return array(
$subscriber_id,
Subscriber::generateToken($subscriber_email),
$queue_id,
$link_hash,
$preview
);
}
static function transformUrlDataObject($data) {
reset($data);
if(!is_int(key($data))) return $data;
$transformed_data = array();
$transformed_data['subscriber_id'] = (!empty($data[0])) ? $data[0] : false;
$transformed_data['subscriber_token'] = (!empty($data[1])) ? $data[1] : false;
$transformed_data['queue_id'] = (!empty($data[2])) ? $data[2] : false;
$transformed_data['link_hash'] = (!empty($data[3])) ? $data[3] : false;
$transformed_data['preview'] = (!empty($data[4])) ? $data[4] : false;
return $transformed_data;
}
}

View File

@ -27,16 +27,19 @@ class Text {
$DOM_parser = new \pQuery();
$DOM = $DOM_parser->parseStr($html);
$blockquotes = $DOM->query('blockquote');
if(!$blockquotes->count()) return $html;
foreach($blockquotes as $blockquote) {
$contents = array();
$paragraphs = $blockquote->query('p', 0);
$paragraphs = $blockquote->query('p, h1, h2, h3, h4', 0);
foreach($paragraphs as $index => $paragraph) {
$contents[] = $paragraph->html();
if($index + 1 < $paragraphs->count()) $contents[] = '<br />';
$paragraph->remove();
if(preg_match('/h\d/', $paragraph->getTag())) {
$contents[] = $paragraph->getOuterText();
} else {
$contents[] = $paragraph->html();
}
if($index + 1 < $paragraphs->count()) $contents[] = '<br />';
$paragraph->remove();
}
$paragraph->remove();
if(empty($contents)) continue;
$blockquote->setTag('table');
$blockquote->addClass('mailpoet_blockquote');
$blockquote->width = '100%';
@ -49,7 +52,7 @@ class Text {
<td width="2" bgcolor="#565656"></td>
<td width="10"></td>
<td valign="top">
<table style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="mailpoet_blockquote">
' . implode('', $contents) . '

View File

@ -21,7 +21,7 @@ class OpenTracking {
}
static function addTrackingImage() {
add_filter(Renderer::POST_PROCESS_FILTER, function ($template) {
add_filter(Renderer::FILTER_POST_PROCESS, function ($template) {
return OpenTracking::process($template);
});
return true;

View File

@ -11,7 +11,7 @@ class Renderer {
public $newsletter;
public $preview;
const NEWSLETTER_TEMPLATE = 'Template.html';
const POST_PROCESS_FILTER = 'mailpoet_rendering_post_process';
const FILTER_POST_PROCESS = 'mailpoet_rendering_post_process';
function __construct($newsletter, $preview = false) {
// TODO: remove ternary condition, refactor to use model objects
@ -24,7 +24,7 @@ class Renderer {
$this->template = file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE);
}
function render() {
function render($type = false) {
$newsletter = $this->newsletter;
$body = (is_array($newsletter['body']))
? $newsletter['body']
@ -48,10 +48,14 @@ class Renderer {
$template = $this->inlineCSSStyles($template);
$template = $this->postProcessTemplate($template);
return array(
$rendered_newsletter = array(
'html' => $template,
'text' => $this->renderTextVersion($template)
);
return ($type && !empty($rendered_newsletter[$type])) ?
$rendered_newsletter[$type] :
$rendered_newsletter;
}
function renderBody($content) {
@ -124,7 +128,7 @@ class Renderer {
str_replace('&', '&amp;', $template->html())
);
$template = apply_filters(
self::POST_PROCESS_FILTER,
self::FILTER_POST_PROCESS,
$DOM->__toString()
);
return $template;

View File

@ -7,19 +7,23 @@ use MailPoet\Statistics\Track\Unsubscribes;
use MailPoet\Subscription\Url as SubscriptionUrl;
class Link {
static function process($action,
static function process(
$action,
$default_value,
$newsletter,
$subscriber,
$queue
$queue,
$content,
$wp_user_preview
) {
switch($action) {
case 'subscription_unsubscribe':
$action = 'subscription_unsubscribe_url';
$url = self::processUrl(
$action,
esc_attr(SubscriptionUrl::getUnsubscribeUrl($subscriber)),
$queue
SubscriptionUrl::getUnsubscribeUrl($subscriber),
$queue,
$wp_user_preview
);
return sprintf(
'<a target="_blank" href="%s">%s</a>',
@ -31,14 +35,16 @@ class Link {
return self::processUrl(
$action,
SubscriptionUrl::getUnsubscribeUrl($subscriber),
$queue
$queue,
$wp_user_preview
);
case 'subscription_manage':
$url = self::processUrl(
$action = 'subscription_manage_url',
esc_attr(SubscriptionUrl::getManageUrl($subscriber)),
$queue
SubscriptionUrl::getManageUrl($subscriber),
$queue,
$wp_user_preview
);
return sprintf(
'<a target="_blank" href="%s">%s</a>',
@ -50,13 +56,20 @@ class Link {
return self::processUrl(
$action,
SubscriptionUrl::getManageUrl($subscriber),
$queue
$queue,
$wp_user_preview
);
case 'newsletter_view_in_browser':
$action = 'newsletter_view_in_browser_url';
$url = esc_attr(NewsletterUrl::getViewInBrowserUrl($newsletter, $subscriber, $queue));
$url = self::processUrl($action, $url, $queue);
$url = NewsletterUrl::getViewInBrowserUrl(
$type = null,
$newsletter,
$subscriber,
$queue,
$wp_user_preview
);
$url = self::processUrl($action, $url, $queue, $wp_user_preview);
return sprintf(
'<a target="_blank" href="%s">%s</a>',
$url,
@ -64,8 +77,14 @@ class Link {
);
case 'newsletter_view_in_browser_url':
$url = NewsletterUrl::getViewInBrowserUrl($newsletter, $subscriber, $queue);
return self::processUrl($action, $url, $queue);
$url = NewsletterUrl::getViewInBrowserUrl(
$type = null,
$newsletter,
$subscriber,
$queue,
$wp_user_preview
);
return self::processUrl($action, $url, $queue, $wp_user_preview);
default:
$shortcode = self::getShortcode($action);
@ -74,15 +93,17 @@ class Link {
$shortcode,
$newsletter,
$subscriber,
$queue
$queue,
$wp_user_preview
);
return ($url !== $shortcode) ?
self::processUrl($action, $url, $queue) :
self::processUrl($action, $url, $queue, $wp_user_preview) :
false;
}
}
static function processUrl($action, $url, $queue) {
static function processUrl($action, $url, $queue, $wp_user_preview = false) {
if($wp_user_preview) return '#';
return ($queue !== false && (boolean)Setting::getValue('tracking.enabled')) ?
self::getShortcode($action) :
$url;
@ -104,7 +125,12 @@ class Link {
$url = SubscriptionUrl::getManageUrl($subscriber);
break;
case 'newsletter_view_in_browser_url':
$url = NewsletterUrl::getViewInBrowserUrl($newsletter, $subscriber, $queue);
$url = NewsletterUrl::getViewInBrowserUrl(
$type = null,
$newsletter,
$subscriber,
$queue
);
break;
default:
$shortcode = self::getShortcode($shortcode_action);

View File

@ -5,16 +5,19 @@ class Shortcodes {
public $newsletter;
public $subscriber;
public $queue;
public $wp_user_preview;
const SHORTCODE_CATEGORY_NAMESPACE = 'MailPoet\Newsletter\Shortcodes\Categories\\';
function __construct(
$newsletter = false,
$subscriber = false,
$queue = false
$queue = false,
$wp_user_preview = false
) {
$this->newsletter = $newsletter;
$this->subscriber = $subscriber;
$this->queue = $queue;
$this->wp_user_preview = $wp_user_preview;
}
function extract($content, $categories = false) {
@ -64,7 +67,8 @@ class Shortcodes {
$_this->newsletter,
$_this->subscriber,
$_this->queue,
$content
$content,
$_this->wp_user_preview
);
return ($custom_shortcode === $shortcode) ?
false :
@ -76,7 +80,8 @@ class Shortcodes {
$_this->newsletter,
$_this->subscriber,
$_this->queue,
$content
$content,
$_this->wp_user_preview
);
}, $shortcodes);
return $processed_shortcodes;

View File

@ -1,52 +1,80 @@
<?php
namespace MailPoet\Newsletter;
use MailPoet\Models\SendingQueue;
use MailPoet\Router\Router;
use MailPoet\Router\Endpoints\ViewInBrowser as ViewInBrowserEndpoint;
use MailPoet\Models\Subscriber;
use MailPoet\Models\Newsletter as NewsletterModel;
use MailPoet\Models\Subscriber as SubscriberModel;
class Url {
const TYPE_ARCHIVE = 'display_archive';
const TYPE_LISTING_EDITOR = 'display_listing_editor';
static function getViewInBrowserUrl(
$newsletter,
$type,
NewsletterModel $newsletter,
$subscriber = false,
$queue = false,
$preview = false
) {
if(is_object($newsletter)) {
$newsletter = $newsletter->asArray();
if($subscriber instanceof SubscriberModel) {
$subscriber->token = SubscriberModel::generateToken($subscriber->email);
}
if(is_object($subscriber)) {
$subscriber = $subscriber->asArray();
} else if(!$subscriber) {
$subscriber = Subscriber::getCurrentWPUser();
$subscriber = ($subscriber) ? $subscriber->asArray() : false;
switch($type) {
case self::TYPE_ARCHIVE:
// do not expose newsletter id when displaying archive newsletters
$newsletter->id = null;
$preview = true;
break;
case self::TYPE_LISTING_EDITOR:
// enable preview and hide newsletter hash when displaying from editor or listings
$newsletter->hash = null;
$preview = true;
break;
default:
// hide hash for all other display types
$newsletter->hash = null;
break;
}
if(is_object($queue)) {
$queue = $queue->asArray();
} else if(!$preview && !empty($newsletter['id'])) {
$queue = SendingQueue::where('newsletter_id', $newsletter['id'])->findOne();
$queue = ($queue) ? $queue->asArray() : false;
}
$data = array(
'newsletter_id' => (!empty($newsletter['id'])) ?
$newsletter['id'] :
$newsletter,
'subscriber_id' => (!empty($subscriber['id'])) ?
$subscriber['id'] :
$subscriber,
'subscriber_token' => (!empty($subscriber['id'])) ?
Subscriber::generateToken($subscriber['email']) :
false,
'queue_id' => (!empty($queue['id'])) ?
$queue['id'] :
$queue,
'preview' => $preview
);
$data = self::createUrlDataObject($newsletter, $subscriber, $queue, $preview);
return Router::buildRequest(
ViewInBrowserEndpoint::ENDPOINT,
ViewInBrowserEndpoint::ACTION_VIEW,
$data
);
}
static function createUrlDataObject($newsletter, $subscriber, $queue, $preview) {
return array(
(!empty($newsletter->id)) ?
(int)$newsletter->id :
0,
(!empty($newsletter->hash)) ?
$newsletter->hash :
0,
(!empty($subscriber->id)) ?
(int)$subscriber->id :
0,
(!empty($subscriber->token)) ?
$subscriber->token :
0,
(!empty($queue->id)) ?
(int)$queue->id :
0,
(int)$preview
);
}
static function transformUrlDataObject($data) {
reset($data);
if (!is_int(key($data))) return $data;
$transformed_data = array();
$transformed_data['newsletter_id'] = (!empty($data[0])) ? $data[0] : false;
$transformed_data['newsletter_hash'] = (!empty($data[1])) ? $data[1] : false;
$transformed_data['subscriber_id'] = (!empty($data[2])) ? $data[2] : false;
$transformed_data['subscriber_token'] = (!empty($data[3])) ? $data[3] : false;
$transformed_data['queue_id'] = (!empty($data[4])) ? $data[4] : false;
$transformed_data['preview'] = (!empty($data[5])) ? $data[5] : false;
return $transformed_data;
}
}

View File

@ -2,13 +2,17 @@
namespace MailPoet\Newsletter;
use MailPoet\Models\Setting;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Links\Links;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Shortcodes\Shortcodes;
class ViewInBrowser {
function view($data) {
$wp_user_preview = ($data->preview && $data->subscriber->isWPUser());
$wp_user_preview = (
($data->subscriber && $data->subscriber->isWPUser() && $data->preview) ||
($data->preview && $data->newsletter_hash)
);
return $this->renderNewsletter(
$data->newsletter,
$data->subscriber,
@ -18,25 +22,36 @@ class ViewInBrowser {
}
function renderNewsletter($newsletter, $subscriber, $queue, $wp_user_preview) {
if($queue && $queue->newsletter_rendered_body) {
$newsletter_body = $queue->getNewsletterRenderedBody();
if($queue && $queue->getNewsletterRenderedBody()) {
$newsletter_body = $queue->getNewsletterRenderedBody('html');
// rendered newsletter body has shortcodes converted to links; we need to
// isolate "view in browser", "unsubscribe" and "manage subscription" links
// and convert them to shortcodes, which later will be replaced with "#" when
// newsletter is previewed
if($wp_user_preview && preg_match(Links::getLinkRegex(), $newsletter_body)) {
$newsletter_body = Links::convertHashedLinksToShortcodesAndUrls(
$newsletter_body,
$convert_all = true
);
// remove open tracking link
$newsletter_body = str_replace(Links::DATA_TAG_OPEN, '', $newsletter_body);
}
} else {
$renderer = new Renderer($newsletter, $wp_user_preview);
$newsletter_body = $renderer->render();
$newsletter_body = $renderer->render('html');
}
$shortcodes = new Shortcodes(
$newsletter,
$subscriber,
$queue
$queue,
$wp_user_preview
);
$rendered_newsletter = $shortcodes->replace($newsletter_body['html']);
if($queue && (boolean)Setting::getValue('tracking.enabled')) {
$rendered_newsletter = $shortcodes->replace($newsletter_body);
if(!$wp_user_preview && $queue && $subscriber && (boolean)Setting::getValue('tracking.enabled')) {
$rendered_newsletter = Links::replaceSubscriberData(
$subscriber->id,
$queue->id,
$rendered_newsletter,
$wp_user_preview
$rendered_newsletter
);
}
return $rendered_newsletter;

View File

@ -5,6 +5,7 @@ use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterLink;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Links\Links;
use MailPoet\Statistics\Track\Clicks;
use MailPoet\Statistics\Track\Opens;
@ -35,10 +36,10 @@ class Track {
}
function _processTrackData($data) {
$data = (object)$data;
$data = (object)Links::transformUrlDataObject($data);
if(empty($data->queue_id) ||
empty($data->subscriber_id) ||
empty($data->subscriber_token)
empty($data->subscriber_id) ||
empty($data->subscriber_token)
) {
return false;
}

View File

@ -1,13 +1,17 @@
<?php
namespace MailPoet\Router\Endpoints;
use MailPoet\Config\Env;
use MailPoet\Models\Newsletter;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Url as NewsletterUrl;
use MailPoet\Newsletter\ViewInBrowser as NewsletterViewInBrowser;
if(!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-includes/pluggable.php');
class ViewInBrowser {
const ENDPOINT = 'view_in_browser';
const ACTION_VIEW = 'view';
@ -24,37 +28,52 @@ class ViewInBrowser {
}
function _processBrowserPreviewData($data) {
$data = (object)$data;
if(empty($data->subscriber_id) ||
empty($data->subscriber_token) ||
empty($data->newsletter_id)
) {
$data = (object)NewsletterUrl::transformUrlDataObject($data);
return ($this->_validateBrowserPreviewData($data)) ?
$data :
$this->_abort();
} else {
$data->newsletter = Newsletter::findOne($data->newsletter_id);
$data->subscriber = Subscriber::findOne($data->subscriber_id);
$data->queue = ($data->queue_id) ?
SendingQueue::findOne($data->queue_id) :
false;
return ($this->_validateBrowserPreviewData($data)) ?
$data :
$this->_abort();
}
}
function _validateBrowserPreviewData($data) {
if(!$data || !$data->subscriber || !$data->newsletter) return false;
$subscriber_token_match =
Subscriber::verifyToken($data->subscriber->email, $data->subscriber_token);
if(!$subscriber_token_match) return false;
// return if this is a WP user previewing the newsletter
if($data->subscriber->isWPUser() && $data->preview) {
return $data;
}
// if queue exists, check if the newsletter was sent to the subscriber
if($data->queue && !$data->queue->isSubscriberProcessed($data->subscriber->id)) {
$data = false;
// either newsletter ID or hash must be defined, and newsletter must exist
if(empty($data->newsletter_id) && empty($data->newsletter_hash)) return false;
$data->newsletter = (!empty($data->newsletter_hash)) ?
Newsletter::getByHash($data->newsletter_hash) :
Newsletter::findOne($data->newsletter_id);
if(!$data->newsletter) return false;
// subscriber is optional; if exists, token must validate
$data->subscriber = (!empty($data->subscriber_id)) ?
Subscriber::findOne($data->subscriber_id) :
false;
if($data->subscriber) {
if(empty($data->subscriber_token) ||
!Subscriber::verifyToken($data->subscriber->email, $data->subscriber_token)
) return false;
}
// if newsletter ID is defined then subscriber must exist
if($data->newsletter_id && !$data->subscriber) return false;
// queue is optional; if defined, get it
$data->queue = (!empty($data->queue_id)) ?
SendingQueue::findOne($data->queue_id) :
SendingQueue::where('newsletter_id', $data->newsletter->id)->findOne();
// allow users with 'manage_options' permission to preview any newsletter
if(!empty($data->preview) && current_user_can(Env::$required_permission)
) return $data;
// allow others to preview newsletters only when newsletter hash is defined
if(!empty($data->preview) && empty($data->newsletter_hash)
) return false;
// if queue and subscriber exist, subscriber must have received the newsletter
if($data->queue &&
$data->subscriber &&
!$data->queue->isSubscriberProcessed($data->subscriber->id)
) return false;
return $data;
}

View File

@ -297,6 +297,10 @@ class Pages {
),
'is_checked' => (
$subscriber->status === Subscriber::STATUS_BOUNCED
),
'is_disabled' => true,
'is_hidden' => (
$subscriber->status !== Subscriber::STATUS_BOUNCED
)
)
)

View File

@ -47,8 +47,13 @@ class Assets
$output = array();
foreach($stylesheets as $stylesheet) {
$output[] = '<link rel="stylesheet" type="text/css"'.
' href="'.$this->_globals['assets_url'].'/css/'.$stylesheet.'">';
$url = $this->appendVersionToUrl(
$this->_globals['assets_url'] . '/css/' . $stylesheet
);
$output[] = sprintf(
'<link rel="stylesheet" type="text/css" href="%s">',
$url
);
}
return join("\n", $output);
@ -59,15 +64,25 @@ class Assets
$output = array();
foreach($scripts as $script) {
$output[] = '<script type="text/javascript"'.
' src="'.$this->_globals['assets_url'].'/js/'.$script.'">'.
'</script>';
$url = $this->appendVersionToUrl(
$this->_globals['assets_url'] . '/js/' . $script
);
$output[] = sprintf(
'<script type="text/javascript" src="%s"></script>',
$url
);
}
return join("\n", $output);
}
public function generateImageUrl($path) {
return $this->_globals['assets_url'].'/img/'.$path;
return $this->appendVersionToUrl(
$this->_globals['assets_url'] . '/img/' . $path
);
}
}
public function appendVersionToUrl($url) {
return add_query_arg('mailpoet_version', $this->_globals['version'], $url);
}
}

View File

@ -1,6 +1,5 @@
<?php
namespace MailPoet\Util;
use \Sunra\PhpSimple\HtmlDomParser;
use csstidy;
/*

49
lib/WP/Readme.php Normal file
View File

@ -0,0 +1,49 @@
<?php
namespace MailPoet\WP;
class Readme {
static function parseChangelog($readme_txt, $limit = null) {
// Extract changelog section of the readme.txt
preg_match('/== Changelog ==(.*?)(\n==|$)/is', $readme_txt, $changelog);
if(empty($changelog[1])) {
return false;
}
// Get changelog entries
$entries = preg_split('/\n(?=\=)/', trim($changelog[1]), -1, PREG_SPLIT_NO_EMPTY);
if(empty($entries)) {
return false;
}
$c = 0;
$changelog = array();
foreach($entries as $entry) {
// Locate version header and changes list
preg_match('/=(.*?)=(.*)/s', $entry, $parts);
if(empty($parts[1]) || empty($parts[2])) {
return false;
}
$header = trim($parts[1]);
$list = trim($parts[2]);
// Get individual items from the list
$list = preg_split('/(^|\n)[\* ]*/', $list, -1, PREG_SPLIT_NO_EMPTY);
$changelog[] = array(
'version' => $header,
'changes' => $list,
);
if(++$c == $limit) {
break;
}
}
return $changelog;
}
}

View File

@ -5,13 +5,13 @@ use MailPoet\Config\Initializer;
/*
* Plugin Name: MailPoet
* Version: 3.0.0-beta.7.1
* Version: 3.0.0-beta.10
* Plugin URI: http://www.mailpoet.com
* Description: Create and send beautiful email newsletters, autoresponders, and post notifications without leaving WordPress. This is a beta version of our brand new plugin!
* Author: MailPoet
* Author URI: http://www.mailpoet.com
* Requires at least: 4.0
* Tested up to: 4.6.1
* Requires at least: 4.6
* Tested up to: 4.7
*
* Text Domain: mailpoet
* Domain Path: /lang/
@ -24,7 +24,7 @@ use MailPoet\Config\Initializer;
$mailpoet_loader = dirname(__FILE__) . '/vendor/autoload.php';
if(file_exists($mailpoet_loader)) {
require $mailpoet_loader;
define('MAILPOET_VERSION', '3.0.0-beta.7.1');
define('MAILPOET_VERSION', '3.0.0-beta.10');
$initializer = new Initializer(
array(
'file' => __FILE__,

View File

@ -2,8 +2,8 @@
Contributors: mailpoet, wysija
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.7.1
Tested up to: 4.7
Stable tag: 3.0.0-beta.10
Create and send beautiful emails and newsletters from WordPress.
== Description ==
@ -83,6 +83,30 @@ Our [support site](https://docs.mailpoet.com/) has plenty of articles. You can w
== Changelog ==
= 3.0.0-beta.10 - 2016-12-27 =
* Improved: newsletter is saved prior to sending an email preview;
* Improved: subscription management page conditionally displays the "bounced" status;
* Improved: deleted lists are displayed in newsletter listings;
* Fixed: newsletter/subscriber/list/form dates are properly formatted according to WP settings;
* Fixed: emails' "Return-path" header is set to the bounce address configured in Settings->Advanced;
* Fixed: archived newsletters' shortcode works for site visitors;
* Fixed: unicode support for newsletters.
= 3.0.0-beta.9 - 2016-12-20 =
* Improved: the plugin is now tested up to WP 4.7;
* Improved: MailPoet's sending service bounce status API update;
* Improved: change duplicate subscribers import message to be more descriptive;
* Fixed: database character set and time zone setup;
* Fixed: alignment of post titles inside notificaiton emails;
* Fixed: partially generated or missing translations from .pot file.
= 3.0.0-beta.8 - 2016-12-13 =
* Added: MailPoet's sending service can now sync hard bounced addresses with the plugin to keep your lists tidy and clean;
* Improved: gracefully catch vendor library conflicts with other plugins. Thx Vikas;
* Improved: force browsers to load the intended JS and CSS assets by adding a parameter, ie style.css?ver=x.y.z;
* Fixed: render non paragraph elements inside a block quote. Thx Remco!;
* Fixed a query that's gone awry in Mysql version 5.6. Dank je Pim!
= 3.0.0-beta.7.1 - 2016-12-06 =
* Improved: allow user to restart sending after sending method failure;
* Fixed: subscribers are not added to lists after import;
@ -136,12 +160,12 @@ Our [support site](https://docs.mailpoet.com/) has plenty of articles. You can w
* Fixed newsletter number shortcode for notification newsletters;
* Enhanced HelpScout support beacon report with extra support data;
* Fixed email renderer to not throw entity warnings on earlier PHP versions;
* Fixed newsletter preview incompatibility errors for earlier PHP versions;
* Fixed newsletter preview incompatibility errors for earlier PHP versions.
= 3.0.0-beta.2 - 2016-10 =
* Fixed compatibility issues with PHP versions earlier than PHP 5.6;
* Renamed 'Emails' email type to 'Newsletters';
* Renamed 'Emails' email type to 'Newsletters'.
= 3.0.0-beta.1 - 2016-10 =

View File

@ -23,10 +23,14 @@ module.exports = function (grunt) {
cwd: '.', // base path where to look for translatable strings
domainPath: 'lang', // where to save the .pot
exclude: [
'build/.*',
'\.mp_svn/.*',
'assets/.*',
'lang/.*',
'node_modules/.*',
'plugin_repository/.*',
'tasks/.*',
'tests/.*',
'vendor/.*',
'tasks/.*'
'vendor/.*'
],
mainFile: 'index.php', // Main project file.
potFilename: 'mailpoet.pot', // Name of the POT file.

View File

@ -257,29 +257,41 @@ class StringExtractor {
$current_argument = null;
}
} elseif(in_array($extension, array('html', 'hbs'))) {
// get all translatable strings
$functions_pattern = '/('.join('|', $function_names).')\((.*)\)/Us';
preg_match_all($functions_pattern, $code, $matches, PREG_OFFSET_CAPTURE);
for($i = 0, $count = count($matches[1]); $i < $count; $i++) {
// get match offset (position where the match was found)
$offset = array_pop($matches[0][$i]);
// fetches all the text before the match
list($before) = str_split($code, $offset);
// calculate line number
$line_number = strlen($before) - strlen(str_replace("\n", "", $before)) + 1;
$function_patterns = array(
'/(__)\((([\'"]).+?\3)\)/',
'/(_n)\(([\'"].+?[\'"],\s*[\'"].+?[\'"],\s*.+?)\)/'
);
// extract arguments (potentially multiple)
$arguments_pattern = "/([^,\\s][^\\,]*)[^,\\s]*/s";
preg_match_all($arguments_pattern, $matches[2][$i][0], $arguments_matches);
$matches = array();
$arguments = array();
foreach($arguments_matches[1] as $arg) {
$arguments[] = trim($arg, "'\"");
}
foreach($function_patterns as $pattern) {
preg_match_all($pattern, $code, $function_matches, PREG_OFFSET_CAPTURE);
for($i = 0; $i < count($function_matches[1]); $i += 1) {
$matches[] = array(
'call' => $function_matches[0][$i][0],
'call_offset' => $function_matches[0][$i][1],
'name' => $function_matches[1][$i][0],
'arguments' => $function_matches[2][$i][0]
);
}
}
foreach($matches as $match) {
list($text_before_match) = str_split($code, $match['call_offset']);
$number_of_newlines = strlen($text_before_match) - strlen(str_replace("\n", "", $text_before_match));
$line_number = $number_of_newlines + 1;
$arguments_pattern = "/(?s)(?<!\\\\)(\"|')(?:[^\\\\]|\\\\.)*?\\1|[^,\\s]+/";
preg_match_all($arguments_pattern, $match['arguments'], $arguments_matches);
$arguments = array();
foreach($arguments_matches[0] as $argument) {
$arguments[] = trim($argument, "'\"");
}
$call = array(
'name' => $matches[1][$i][0],
'name' => $match['name'],
'args' => $arguments,
'line' => $line_number
);

View File

@ -90,6 +90,16 @@ define([
mock.verify();
});
it('provides a promise if a result container is passed to save event', function() {
var spy = sinon.spy(module, 'save'),
saveResult = {promise: null};
module.saveAndProvidePromise(saveResult);
spy.restore();
expect(spy.calledOnce).to.be.true;
expect(saveResult.promise).to.be.an('object');
expect(saveResult.promise.then).to.be.a('function');
});
});
describe('view', function() {

View File

@ -59,4 +59,10 @@ class EnvTest extends MailPoetTest {
ENV::$db_host . ';port=' . ENV::$db_port . ';dbname=' . DB_NAME;
expect(Env::$db_source_name)->equals($source_name);
}
}
function testItCanGetDbTimezoneOffset() {
expect(Env::getDbTimezoneOffset('+1.5'))->equals("+01:30");
expect(Env::getDbTimezoneOffset('+11'))->equals("+11:00");
expect(Env::getDbTimezoneOffset('-5.5'))->equals("-05:30");
}
}

View File

@ -0,0 +1,19 @@
<?php
use MailPoet\Config\Env;
class InitializerTest extends MailPoetTest {
function testItSetsDBDriverOptions() {
$result = ORM::for_table("")
->raw_query(
'SELECT ' .
'@@sql_mode as sql_mode, ' .
'@@session.time_zone as time_zone'
)
->findOne();
// disable ONLY_FULL_GROUP_BY
expect($result->sql_mode)->notContains('ONLY_FULL_GROUP_BY');
// time zone should be set based on WP's time zone
expect($result->time_zone)->equals(Env::$db_timezone_offset);
}
}

View File

@ -0,0 +1,38 @@
<?php
use MailPoet\Config\Shortcodes;
use MailPoet\Models\Newsletter;
use MailPoet\Models\SendingQueue;
use MailPoet\Newsletter\Url;
use MailPoet\Router\Router;
class ConfigShortcodesTest extends MailPoetTest {
function _before() {
$newsletter = Newsletter::create();
$newsletter->type = Newsletter::TYPE_STANDARD;
$newsletter->status = Newsletter::STATUS_SENT;
$this->newsletter = $newsletter->save();
$queue = SendingQueue::create();
$queue->newsletter_id = $newsletter->id;
$queue->status = SendingQueue::STATUS_COMPLETED;
$this->queue = $queue->save();
}
function testItGetsArchives() {
$shortcodes = new Shortcodes();
// result contains a link pointing to the "view in browser" router endpoint
$result = $shortcodes->getArchive($params = false);
$dom = pQuery::parseStr($result);
$link = $dom->query('a');
$link = $link->attr('href');
expect($link)->contains('endpoint=view_in_browser');
// request data object contains newsletter hash but not newsletter id
$parsed_link = parse_url($link);
parse_str(html_entity_decode($parsed_link['query']), $data);
$request_data = Url::transformUrlDataObject(
Router::decodeRequestData($data['data'])
);
expect($request_data['newsletter_id'])->isEmpty();
expect($request_data['newsletter_hash'])->equals($this->newsletter->hash);
}
}

View File

@ -0,0 +1,208 @@
<?php
use Carbon\Carbon;
use Codeception\Util\Stub;
use MailPoet\API\Endpoints\Cron;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\Bounce;
use MailPoet\Cron\Workers\Bounce\API;
use MailPoet\Mailer\Mailer;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Setting;
use MailPoet\Models\Subscriber;
use MailPoet\Util\Helpers;
require_once('BounceTestMockAPI.php');
class BounceTest extends MailPoetTest {
function _before() {
$this->emails = array(
'soft_bounce@example.com',
'hard_bounce@example.com',
'good_address@example.com'
);
foreach ($this->emails as $email) {
Subscriber::createOrUpdate(array(
'status' => Subscriber::STATUS_SUBSCRIBED,
'email' => $email
));
}
$this->bounce = new Bounce(microtime(true));
$api =
$this->bounce->api = new MailPoet\Cron\Workers\Bounce\MockAPI('key');
}
function testItConstructs() {
expect($this->bounce->timer)->notEmpty();
}
function testItDefinesConstants() {
expect(Bounce::BATCH_SIZE)->equals(100);
}
function testItChecksIfCurrentSendingMethodIsMailpoet() {
expect(Bounce::checkBounceSyncAvailable())->false();
$this->setMailPoetSendingMethod();
expect(Bounce::checkBounceSyncAvailable())->true();
}
function testItThrowsExceptionWhenExecutionLimitIsReached() {
try {
$bounce = new Bounce(microtime(true) - CronHelper::DAEMON_EXECUTION_LIMIT);
self::fail('Maximum execution time limit exception was not thrown.');
} catch(\Exception $e) {
expect($e->getMessage())->equals('Maximum execution time has been reached.');
}
}
function testItSchedulesBounceSync() {
expect(SendingQueue::where('type', 'bounce')->findMany())->isEmpty();
Bounce::scheduleBounceSync();
expect(SendingQueue::where('type', 'bounce')->findMany())->notEmpty();
}
function testItDoesNotScheduleBounceSyncTwice() {
expect(count(SendingQueue::where('type', 'bounce')->findMany()))->equals(0);
Bounce::scheduleBounceSync();
expect(count(SendingQueue::where('type', 'bounce')->findMany()))->equals(1);
Bounce::scheduleBounceSync();
expect(count(SendingQueue::where('type', 'bounce')->findMany()))->equals(1);
}
function testItCanGetScheduledQueues() {
expect(Bounce::getScheduledQueues())->isEmpty();
$this->createScheduledQueue();
expect(Bounce::getScheduledQueues())->notEmpty();
}
function testItCanGetRunningQueues() {
expect(Bounce::getRunningQueues())->isEmpty();
$this->createRunningQueue();
expect(Bounce::getRunningQueues())->notEmpty();
}
function testItCanGetAllDueQueues() {
expect(Bounce::getAllDueQueues())->isEmpty();
// scheduled for now
$this->createScheduledQueue();
// running
$this->createRunningQueue();
// scheduled in the future (should not be retrieved)
$queue = $this->createScheduledQueue();
$queue->scheduled_at = Carbon::createFromTimestamp(current_time('timestamp'))->addDays(7);
$queue->save();
// completed (should not be retrieved)
$queue = $this->createRunningQueue();
$queue->status = SendingQueue::STATUS_COMPLETED;
$queue->save();
expect(count(Bounce::getAllDueQueues()))->equals(2);
}
function testItCanGetFutureQueues() {
expect(Bounce::getFutureQueues())->isEmpty();
$queue = $this->createScheduledQueue();
$queue->scheduled_at = Carbon::createFromTimestamp(current_time('timestamp'))->addDays(7);
$queue->save();
expect(count(Bounce::getFutureQueues()))->notEmpty();
}
function testItFailsToProcessWithoutMailPoetMethodSetUp() {
expect($this->bounce->process())->false();
}
function testItFailsToProcessWithoutQueues() {
$this->setMailPoetSendingMethod();
expect($this->bounce->process())->false();
}
function testItProcesses() {
$this->setMailPoetSendingMethod();
$this->createScheduledQueue();
$this->createRunningQueue();
expect($this->bounce->process())->true();
}
function testItPreparesBounceQueue() {
$queue = $this->createScheduledQueue();
expect(empty($queue->subscribers['to_process']))->true();
$this->bounce->prepareBounceQueue($queue);
expect($queue->status)->null();
expect(!empty($queue->subscribers['to_process']))->true();
}
function testItProcessesBounceQueue() {
$queue = $this->createRunningQueue();
$this->bounce->prepareBounceQueue($queue);
expect(!empty($queue->subscribers['to_process']))->true();
$this->bounce->processBounceQueue($queue);
expect(!empty($queue->subscribers['processed']))->true();
}
function testItSetsSubscriberStatusAsBounced() {
$emails = Subscriber::select('email')->findArray();
$emails = Helpers::arrayColumn($emails, 'email');
$this->bounce->processEmails($emails);
$subscribers = Subscriber::findMany();
expect($subscribers[0]->status)->equals(Subscriber::STATUS_SUBSCRIBED);
expect($subscribers[1]->status)->equals(Subscriber::STATUS_BOUNCED);
expect($subscribers[2]->status)->equals(Subscriber::STATUS_SUBSCRIBED);
}
function testItCalculatesNextRunDateWithinNextWeekBoundaries() {
$current_date = Carbon::createFromTimestamp(current_time('timestamp'));
$next_run_date = Bounce::getNextRunDate();
$difference = $next_run_date->diffInDays($current_date);
// Subtract days left in the current week
$difference -= (Carbon::DAYS_PER_WEEK - $current_date->format('N'));
expect($difference)->lessOrEquals(7);
expect($difference)->greaterOrEquals(0);
}
private function setMailPoetSendingMethod() {
Setting::setValue(
Mailer::MAILER_CONFIG_SETTING_NAME,
array(
'method' => 'MailPoet',
'mailpoet_api_key' => 'some_key',
)
);
}
private function createScheduledQueue() {
$queue = SendingQueue::create();
$queue->type = 'bounce';
$queue->status = SendingQueue::STATUS_SCHEDULED;
$queue->scheduled_at = Carbon::createFromTimestamp(current_time('timestamp'));
$queue->newsletter_id = 0;
$queue->save();
return $queue;
}
private function createRunningQueue() {
$queue = SendingQueue::create();
$queue->type = 'bounce';
$queue->status = null;
$queue->scheduled_at = Carbon::createFromTimestamp(current_time('timestamp'));
$queue->newsletter_id = 0;
$queue->save();
return $queue;
}
function _after() {
ORM::raw_execute('TRUNCATE ' . Setting::$_table);
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace MailPoet\Cron\Workers\Bounce;
if(!defined('ABSPATH')) exit;
class MockAPI {
function check(array $emails) {
return array_map(
function ($email) {
return array(
'address' => $email,
'bounce' => preg_match('/(hard|soft)/', $email, $m) ? $m[1] : null,
);
},
$emails
);
}
}

View File

@ -0,0 +1,64 @@
<?php
use MailPoet\Form\Block\Select;
use \MailPoet\Models\Subscriber;
class SelectTest extends MailPoetTest {
function _before() {
$this->block = array(
'id' => 'status',
'type' => 'select',
'params' => array(
'required' => true,
'label' => 'Status',
'values' => array(
array(
'value' => array(
Subscriber::STATUS_SUBSCRIBED => Subscriber::STATUS_SUBSCRIBED
),
'is_checked' => false
),
array(
'value' => array(
Subscriber::STATUS_UNSUBSCRIBED => Subscriber::STATUS_UNSUBSCRIBED
),
'is_checked' => false
),
array(
'value' => array(
Subscriber::STATUS_BOUNCED => Subscriber::STATUS_BOUNCED
),
'is_checked' => false,
'is_disabled' => false,
'is_hidden' => false
)
)
)
);
}
function testItRendersSelectBlock() {
$rendered = Select::render($this->block);
expect($rendered)->contains(Subscriber::STATUS_SUBSCRIBED);
expect($rendered)->contains(Subscriber::STATUS_UNSUBSCRIBED);
expect($rendered)->contains(Subscriber::STATUS_BOUNCED);
}
function testItRendersSelectedOption() {
$this->block['params']['values'][0]['is_checked'] = true;
$rendered = Select::render($this->block);
expect($rendered)->contains('selected="selected"');
}
function testItRendersDisabledOptions() {
$this->block['params']['values'][2]['is_disabled'] = true;
$rendered = Select::render($this->block);
expect($rendered)->contains('disabled="disabled"');
}
function testItDoesNotRenderHiddenOptions() {
$this->block['params']['values'][2]['is_hidden'] = true;
$rendered = Select::render($this->block);
expect($rendered)->notContains(Subscriber::STATUS_BOUNCED);
}
}

View File

@ -40,6 +40,7 @@ class MailerTest extends MailPoetTest {
'name' => 'Reply To',
'address' => 'staff@mailinator.com'
);
$this->return_path = 'bounce@test.com';
$this->mailer = array(
'method' => 'MailPoet',
'mailpoet_api_key' => getenv('WP_TEST_MAILER_MAILPOET_API') ?
@ -59,7 +60,6 @@ class MailerTest extends MailPoetTest {
function testItRequiresMailerMethod() {
// reset mta settings so that we have no default mailer
Setting::setValue('mta', null);
try {
$mailer = new Mailer();
$this->fail('Mailer did not throw an exception');
@ -78,11 +78,12 @@ class MailerTest extends MailPoetTest {
}
function testItCanConstruct() {
$mailer = new Mailer($this->mailer, $this->sender, $this->reply_to);
$mailer = new Mailer($this->mailer, $this->sender, $this->reply_to, $this->return_path);
expect($mailer->sender['from_name'])->equals($this->sender['name']);
expect($mailer->sender['from_email'])->equals($this->sender['address']);
expect($mailer->reply_to['reply_to_name'])->equals($this->reply_to['name']);
expect($mailer->reply_to['reply_to_email'])->equals($this->reply_to['address']);
expect($mailer->return_path)->equals($this->return_path);
}
function testItCanBuildKnownMailerInstances() {
@ -103,7 +104,6 @@ class MailerTest extends MailPoetTest {
}
}
function testItSetsReplyToAddressWhenOnlyNameIsAvailable() {
$reply_to = array('name' => 'test');
$mailer = new Mailer($this->mailer, $this->sender, $reply_to);
@ -111,6 +111,15 @@ class MailerTest extends MailPoetTest {
expect($reply_to['reply_to_email'])->equals($this->sender['address']);
}
function testItGetsReturnPathAddress() {
$mailer = new Mailer($this->mailer, $this->sender, $this->reply_to);
$return_path = $mailer->getReturnPathAddress('bounce@test.com');
expect($return_path)->equals('bounce@test.com');
Setting::setValue('bounce', array('address' => 'settngs_bounce@test.com'));
$return_path = $mailer->getReturnPathAddress($return_path = false);
expect($return_path)->equals('settngs_bounce@test.com');
}
function testItCanTransformSubscriber() {
$mailer = new Mailer($this->mailer, $this->sender, $this->reply_to);
expect($mailer->formatSubscriberNameAndEmailAddress('test@email.com'))

View File

@ -26,12 +26,14 @@ class AmazonSESTest extends MailPoetTest {
'reply_to_email' => 'reply-to@mailpoet.com',
'reply_to_name_email' => 'Reply To <reply-to@mailpoet.com>'
);
$this->return_path = 'bounce@mailpoet.com';
$this->mailer = new AmazonSES(
$this->settings['region'],
$this->settings['access_key'],
$this->settings['secret_key'],
$this->sender,
$this->reply_to
$this->reply_to,
$this->return_path
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
@ -56,6 +58,18 @@ class AmazonSESTest extends MailPoetTest {
expect(preg_match('!^\d{8}$!', $this->mailer->date_without_time))->equals(1);
}
function testWhenReturnPathIsNullItIsSetToSenderEmail() {
$mailer = new AmazonSES(
$this->settings['region'],
$this->settings['access_key'],
$this->settings['secret_key'],
$this->sender,
$this->reply_to,
$return_path = false
);
expect($mailer->return_path)->equals($this->sender['from_email']);
}
function testItChecksForValidRegion() {
try {
$mailer = new AmazonSES(
@ -63,7 +77,8 @@ class AmazonSESTest extends MailPoetTest {
$this->settings['access_key'],
$this->settings['secret_key'],
$this->sender,
$this->reply_to
$this->reply_to,
$this->return_path
);
$this->fail('Unsupported region exception was not thrown');
} catch(\Exception $e) {
@ -86,7 +101,7 @@ class AmazonSESTest extends MailPoetTest {
->equals($this->newsletter['body']['html']);
expect($body['Message.Body.Text.Data'])
->equals($this->newsletter['body']['text']);
expect($body['ReturnPath'])->equals($this->sender['from_name_email']);
expect($body['ReturnPath'])->equals($this->return_path);
}
function testItCanCreateRequest() {

View File

@ -14,9 +14,11 @@ class PHPMailTest extends MailPoetTest {
'reply_to_email' => 'reply-to@mailpoet.com',
'reply_to_name_email' => 'Reply To <reply-to@mailpoet.com>'
);
$this->return_path = 'bounce@mailpoet.com';
$this->mailer = new PHPMail(
$this->sender,
$this->reply_to
$this->reply_to,
$this->return_path
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
@ -33,6 +35,15 @@ class PHPMailTest extends MailPoetTest {
expect($mailer->getTransport() instanceof \Swift_MailTransport)->true();
}
function testWhenReturnPathIsNullItIsSetToSenderEmail() {
$mailer = new PHPMail(
$this->sender,
$this->reply_to,
$return_path = false
);
expect($mailer->return_path)->equals($this->sender['from_email']);
}
function testItCanCreateMessage() {
$message = $this->mailer->createMessage($this->newsletter, $this->subscriber);
expect($message->getTo())

View File

@ -29,6 +29,7 @@ class SMTPTest extends MailPoetTest {
'reply_to_email' => 'reply-to@mailpoet.com',
'reply_to_name_email' => 'Reply To <reply-to@mailpoet.com>'
);
$this->return_path = 'bounce@mailpoet.com';
$this->mailer = new SMTP(
$this->settings['host'],
$this->settings['port'],
@ -37,7 +38,8 @@ class SMTPTest extends MailPoetTest {
$this->settings['password'],
$this->settings['encryption'],
$this->sender,
$this->reply_to
$this->reply_to,
$this->return_path
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
@ -63,6 +65,21 @@ class SMTPTest extends MailPoetTest {
->equals($this->settings['encryption']);
}
function testWhenReturnPathIsNullItIsSetToSenderEmail() {
$mailer = new SMTP(
$this->settings['host'],
$this->settings['port'],
$this->settings['authentication'],
$this->settings['login'],
$this->settings['password'],
$this->settings['encryption'],
$this->sender,
$this->reply_to,
$return_path = false
);
expect($mailer->return_path)->equals($this->sender['from_email']);
}
function testItCanCreateMessage() {
$message = $this->mailer->createMessage($this->newsletter, $this->subscriber);
expect($message->getTo())

View File

@ -106,6 +106,17 @@ class NewsletterTest extends MailPoetTest {
expect($newsletter_segments[1]['name'])->equals('Segment 2');
}
function testItCanHaveDeletedSegments() {
$this->segment_2->delete();
$this->newsletter->withSegments(true);
$newsletter_segments = $this->newsletter->segments;
expect($newsletter_segments)->count(2);
expect($newsletter_segments[0]['id'])->equals($this->segment_1->id);
expect($newsletter_segments[0]['name'])->equals('Segment 1');
expect($newsletter_segments[1]['id'])->equals($this->segment_2->id);
expect($newsletter_segments[1]['name'])->contains('Deleted');
}
function testItCanHaveStatistics() {
$newsletter = $this->newsletter;
$sending_queue = SendingQueue::create();
@ -245,6 +256,118 @@ class NewsletterTest extends MailPoetTest {
expect($newsletter->Event)->equals($association->value);
}
function testItGetsArchiveNewslettersForSegments() {
// clear the DB
$this->_after();
$types = array(
Newsletter::TYPE_STANDARD,
Newsletter::TYPE_NOTIFICATION_HISTORY
);
$newsletters = array();
$sending_queues[] = array();
for($i = 0; $i < count($types); $i++) {
$newsletters[$i] = Newsletter::createOrUpdate(
array(
'subject' => 'My Standard Newsletter',
'preheader' => 'Pre Header',
'type' => $types[$i]
)
);
$sending_queues[$i] = SendingQueue::create();
$sending_queues[$i]->newsletter_id = $newsletters[$i]->id;
$sending_queues[$i]->status = SendingQueue::STATUS_COMPLETED;
$sending_queues[$i]->save();
}
// set segment association for the last newsletter
$newsletter_segment = NewsletterSegment::create();
$newsletter_segment->newsletter_id = end($newsletters[1])->id;
$newsletter_segment->segment_id = 123;
$newsletter_segment->save();
expect(Newsletter::findMany())->count(2);
// return archives in segment 123
$results = Newsletter::getArchives(array(123));
expect($results)->count(1);
expect($results[0]->id)->equals($newsletters[1]->id);
expect($results[0]->type)->equals(Newsletter::TYPE_NOTIFICATION_HISTORY);
}
function testItGetsAllArchiveNewsletters() {
// clear the DB
$this->_after();
$types = array(
Newsletter::TYPE_STANDARD,
Newsletter::TYPE_STANDARD, // should be returned
Newsletter::TYPE_WELCOME,
Newsletter::TYPE_NOTIFICATION,
Newsletter::TYPE_NOTIFICATION_HISTORY, // should be returned
Newsletter::TYPE_NOTIFICATION_HISTORY
);
$newsletters = array();
$sending_queues[] = array();
for($i = 0; $i < count($types); $i++) {
$newsletters[$i] = Newsletter::createOrUpdate(
array(
'subject' => 'My Standard Newsletter',
'preheader' => 'Pre Header',
'type' => $types[$i]
)
);
$sending_queues[$i] = SendingQueue::create();
$sending_queues[$i]->newsletter_id = $newsletters[$i]->id;
$sending_queues[$i]->status = SendingQueue::STATUS_COMPLETED;
$sending_queues[$i]->save();
}
// set teh sending queue status of the first newsletter to null
$sending_queues[0]->status = null;
$sending_queues[0]->save();
// trash the last newsletter
end($newsletters)->trash();
expect(Newsletter::findMany())->count(6);
// archives return only:
// 1. STANDARD and NOTIFICATION HISTORY newsletters
// 2. active newsletters (i.e., not trashed)
// 3. with sending queue records that are COMPLETED
$results = Newsletter::getArchives();
expect($results)->count(2);
expect($results[0]->id)->equals($newsletters[1]->id);
expect($results[0]->type)->equals(Newsletter::TYPE_STANDARD);
expect($results[1]->id)->equals($newsletters[4]->id);
expect($results[1]->type)->equals(Newsletter::TYPE_NOTIFICATION_HISTORY);
}
function testItGeneratesHashOnNewsletterSave() {
expect(strlen($this->newsletter->hash))
->equals(Newsletter::NEWSLETTER_HASH_LENGTH);
}
function testItRegeneratesHashOnNewsletterDuplication() {
$duplicate_newsletter = $this->newsletter->duplicate();
expect($duplicate_newsletter->hash)->notEquals($this->newsletter->hash);
expect(strlen($duplicate_newsletter->hash))
->equals(Newsletter::NEWSLETTER_HASH_LENGTH);
}
function testItRegeneratesHashOnNotificationHistoryCreation() {
$notification_history = $this->newsletter->createNotificationHistory();
expect($notification_history->hash)->notEquals($this->newsletter->hash);
expect(strlen($notification_history->hash))
->equals(Newsletter::NEWSLETTER_HASH_LENGTH);
}
function testItGetsQueueFromNewsletter() {
$queue = SendingQueue::create();
$queue->newsletter_id = $this->newsletter->id;
$queue->save();
expect($this->newsletter->queue()->findOne()->id)->equals($queue->id);
}
function _after() {
ORM::raw_execute('TRUNCATE ' . NewsletterOption::$_table);
ORM::raw_execute('TRUNCATE ' . Newsletter::$_table);
@ -254,4 +377,4 @@ class NewsletterTest extends MailPoetTest {
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);
ORM::raw_execute('TRUNCATE ' . StatisticsOpens::$_table);
}
}
}

View File

@ -9,7 +9,6 @@ use MailPoet\Models\SubscriberCustomField;
use MailPoet\Models\SubscriberSegment;
class SubscriberTest extends MailPoetTest {
function _before() {
$this->data = array(
'first_name' => 'John',
@ -555,6 +554,23 @@ class SubscriberTest extends MailPoetTest {
expect($total)->equals(1);
}
function testItGeneratesSubscriberToken() {
$token = Subscriber::generateToken($this->data['email']);
expect(strlen($token))->equals(Subscriber::SUBSCRIBER_TOKEN_LENGTH);
}
function testItVerifiesSubscriberToken() {
$token = Subscriber::generateToken($this->data['email']);
expect(Subscriber::verifyToken($this->data['email'], $token))->true();
expect(Subscriber::verifyToken('fake@email.com', $token))->false();
}
function testVerifiedTokensOfDifferentLengths() {
$token = md5(AUTH_KEY . $this->data['email']);
expect(strlen($token))->notEquals(Subscriber::SUBSCRIBER_TOKEN_LENGTH);
expect(Subscriber::verifyToken($this->data['email'], $token))->true();
}
function _after() {
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . Segment::$_table);
@ -562,4 +578,4 @@ class SubscriberTest extends MailPoetTest {
ORM::raw_execute('TRUNCATE ' . CustomField::$_table);
ORM::raw_execute('TRUNCATE ' . SubscriberCustomField::$_table);
}
}
}

View File

@ -5,6 +5,7 @@ use MailPoet\Models\NewsletterLink;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Links\Links;
use MailPoet\Newsletter\Shortcodes\Categories\Link;
use MailPoet\Router\Router;
class LinksTest extends MailPoetTest {
@ -46,6 +47,29 @@ class LinksTest extends MailPoetTest {
expect($updated_content)->notContains('link');
}
function testItCreatesAndTransformsUrlDataObject() {
$subscriber_email = 'test@example.com';
$data = array(
'subscriber_id' => 1,
'subscriber_token' => Subscriber::generateToken($subscriber_email),
'queue_id' => 2,
'link_hash' => 'hash',
'preview' => false
);
$url_data_object = Links::createUrlDataObject(
$data['subscriber_id'],
$subscriber_email,
$data['queue_id'],
$data['link_hash'],
$data['preview']
);
// URL data object should be an indexed array
expect($url_data_object)->equals(array_values($data));
// transformed URL object should be an associative array
$transformed_url_data_object = Links::transformUrlDataObject($url_data_object);
expect($transformed_url_data_object)->equals($data);
}
function testItReplacesHashedLinksWithSubscriberData() {
$subscriber = Subscriber::create();
$subscriber->hydrate(Fixtures::get('subscriber_template'));
@ -70,6 +94,7 @@ class LinksTest extends MailPoetTest {
preg_match_all('/data=(?P<data>.*?)"/', $result, $result);
foreach($result['data'] as $data) {
$data = Router::decodeRequestData($data);
$data = Links::transformUrlDataObject($data);
expect($data['subscriber_id'])->equals($subscriber->id);
expect($data['queue_id'])->equals($queue->id);
expect(isset($data['subscriber_token']))->true();
@ -97,6 +122,52 @@ class LinksTest extends MailPoetTest {
expect($newsltter_link->url)->equals('http://example.com');
}
function testItMatchesHashedLinks() {
$regex = Links::getLinkRegex();
expect((boolean)preg_match($regex, '[some_tag]-123'))->false();
expect((boolean)preg_match($regex, '[some_tag]'))->false();
expect((boolean)preg_match($regex, '[mailpoet_click_data]-123'))->true();
expect((boolean)preg_match($regex, '[mailpoet_open_data]'))->true();
}
function testItCanConvertOnlyHashedLinkShortcodes() {
// create newsletter link association
$newsletter_link = NewsletterLink::create();
$newsletter_link->newsletter_id = 1;
$newsletter_link->queue_id = 1;
$newsletter_link->hash = '90e56';
$newsletter_link->url = '[link:newsletter_view_in_browser_url]';
$newsletter_link = $newsletter_link->save();
$content = '
<a href="[mailpoet_click_data]-90e56">View in browser</a>
<a href="[mailpoet_click_data]-123">Some link</a>';
$result = Links::convertHashedLinksToShortcodesAndUrls($content);
expect($result)->contains($newsletter_link->url);
expect($result)->contains('[mailpoet_click_data]-123');
}
function testItCanConvertAllHashedLinksToUrls() {
// create newsletter link associations
$newsletter_link_1 = NewsletterLink::create();
$newsletter_link_1->newsletter_id = 1;
$newsletter_link_1->queue_id = 1;
$newsletter_link_1->hash = '90e56';
$newsletter_link_1->url = '[link:newsletter_view_in_browser_url]';
$newsletter_link_1 = $newsletter_link_1->save();
$newsletter_link_2 = NewsletterLink::create();
$newsletter_link_2->newsletter_id = 1;
$newsletter_link_2->queue_id = 1;
$newsletter_link_2->hash = '123';
$newsletter_link_2->url = 'http://google.com';
$newsletter_link_2 = $newsletter_link_2->save();
$content = '
<a href="[mailpoet_click_data]-90e56">View in browser</a>
<a href="[mailpoet_click_data]-123">Some link</a>';
$result = Links::convertHashedLinksToShortcodesAndUrls($content, $convert_all = true);
expect($result)->contains($newsletter_link_1->url);
expect($result)->contains($newsletter_link_2->url);
}
function _after() {
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);

View File

@ -212,6 +212,13 @@ class NewsletterRendererTest extends MailPoetTest {
expect(
!empty($DOM('tr > td > table > tr > td.mailpoet_blockquote', 0)->html()
))->true();
// blockquote should contain heading elements but not paragraphs
expect(
$DOM('tr > td > table > tr > td.mailpoet_blockquote', 0)->html()
)->contains('<h2');
expect(
$DOM('tr > td > table > tr > td.mailpoet_blockquote', 0)->html()
)->notContains('<p');
// ul/ol/li should have mailpoet_paragraph class added & styles applied
expect(
!empty(
@ -366,4 +373,4 @@ class NewsletterRendererTest extends MailPoetTest {
expect(preg_match('/mailpoet_template.*?important/s', $template['html']))
->equals(0);
}
}
}

View File

@ -55,7 +55,7 @@
},
{
"type": "text",
"text": "<h1 style=\"text-align: left;\">1/1 Column</h1>\n<h1 style=\"text-align: center;\">1/1 Column</h1>\n<h1 style=\"text-align: right;\">1/1 Column</h1>\n<h1><a href=\"http://www.example.com\">1/1 Column</a></h1>\n<h2>Heading (size 2)</h2>\n<p>Paragraph under heading to test line-height.</p>\n<h3>Heading (size 3)</h3>\n<h3>Heading (size 3) <a href=\"http://www.example.org\">with link</a></h3>\n<h3><strong>Heading</strong> (size 3)</h3>\n<p>Paragraph under heading to test line-height.</p>\n<p style=\"text-align: left;\"><strong>Bacon ipsum dolor amet short ribs shank cow, ribeye corned beef short loin t-bone kielbasa meatloaf ball tip rump venison boudin brisket beef ribs. Fatback landjaeger frankfurter, meatloaf picanha andouille leberkas.</strong> <em>Tail beef ribs boudin salami, kevin cupim landjaeger pork loin tenderloin ham filet mignon drumstick short loin. Biltong frankfurter shank pork belly picanha prosciutto meatloaf tail hamburger landjaeger pancetta shankle pig.</em> Pig tri-tip tenderloin ground round ribeye alcatra turkey salami turducken sausage pork loin kielbasa hamburger meatloaf strip steak.</p>\n<p style=\"text-align: left;\"><strong>Second paragraph, same text-alignment goes in the same table, other text alignments go into new tables, see?</strong> <em>Tail beef ribs boudin salami, kevin cupim landjaeger pork loin tenderloin ham filet mignon drumstick short loin. Ribeye boudin cow, beef ribs t-bone pig short ribs tri-tip pork loin rump shank hamburger short loin. Salami pastrami meatball shoulder cupim.</em></p>\n<p style=\"text-align: center;\">Bacon kevin shank ball tip shoulder. Jowl leberkas fatback, short loin chuck beef beef ribs short ribs ribeye turducken pork chop brisket filet mignon cow. Turkey ball tip rump bacon filet mignon sausage jowl shoulder chicken ground round kielbasa shankle. <em>Drumstick pancetta corned beef kielbasa porchetta jerky swine leberkas kevin boudin chicken shoulder bacon tri-tip venison.</em> Ham hock ball tip beef ribs spare ribs tail pork ground round, biltong doner t-bone pork chop rump hamburger pancetta brisket.</p>\n<p style=\"text-align: center;\"><strong>Second paragraph, same text-alignment goes in the same table, other text alignments go into new tables, see?</strong> <em>Tail beef ribs boudin salami, <span style=\"color: #ffff00;\">kevin cupim landjaeger pork loin tenderloin</span> ham filet mignon drumstick short loin. Ribeye boudin cow, beef ribs t-bone pig short ribs tri-tip pork loin rump shank hamburger short loin. Salami pastrami meatball shoulder cupim.</em></p>\n<p style=\"text-align: right;\">Brisket beef kielbasa jowl hamburger, doner flank. Shoulder ham hock sausage t-bone pork belly chicken picanha pork loin ham bresaola tri-tip ground round kevin. <em>Chicken sirloin shankle fatback boudin t-bone pig tri-tip bresaola doner cow short loin pancetta short ribs andouille. Cupim doner short ribs, andouille cow t-bone ground round pork porchetta beef capicola.</em> Rump drumstick biltong shank kielbasa bacon ball tip pancetta meatloaf shankle fatback.</p>\n<p style=\"text-align: right;\"><strong>Second paragraph, same text-alignment goes in the same table, other text alignments go into new tables, see?</strong> <em>Tail beef ribs boudin salami, kevin cupim landjaeger pork loin tenderloin ham filet mignon drumstick short loin. Ribeye boudin cow, beef ribs t-bone pig short ribs tri-tip pork loin rump shank hamburger short loin. Salami pastrami meatball shoulder cupim.</em></p>\n<p style=\"text-align: justify;\">Kielbasa jowl flank biltong. Pork loin fatback chicken ham prosciutto sausage cow short loin porchetta kielbasa. <em>Bresaola ham hock pancetta, cow ham tenderloin flank turducken fatback beef jowl short loin pig.</em> Picanha turkey spare ribs capicola andouille, tongue short loin sausage corned beef kevin meatball venison kielbasa pastrami. Beef ribs ground round tenderloin flank.</p>\n<p style=\"text-align: justify;\"><strong>Second paragraph, same text-alignment goes in the same table, other text alignments go into new tables, see?</strong> <em>Tail beef ribs boudin salami, kevin cupim landjaeger pork loin tenderloin ham filet mignon drumstick short loin. Ribeye boudin cow, beef ribs t-bone pig short ribs tri-tip pork loin rump shank hamburger short loin. Salami pastrami meatball shoulder cupim.</em> <a href=\"http://www.example.org\">Alcatra hamburger</a><br /><br /></p>\n<ul>\n<li>One</li>\n<li>Two</li>\n<li>Three</li>\n</ul>\n<ol>\n<li>One</li>\n<li>Two</li>\n<li>Three</li>\n</ol>\n<blockquote>\n<p>Bacon ipsum dolor amet shoulder turkey meatball pork chop porchetta, filet mignon shankle. Sausage meatloaf flank picanha jowl chuck capicola tri-tip. Meatloaf andouille kielbasa beef ribs.</p>\n</blockquote>"
"text": "<h1 style=\"text-align: left;\">1/1 Column</h1>\n<h1 style=\"text-align: center;\">1/1 Column</h1>\n<h1 style=\"text-align: right;\">1/1 Column</h1>\n<h1><a href=\"http://www.example.com\">1/1 Column</a></h1>\n<h2>Heading (size 2)</h2>\n<p>Paragraph under heading to test line-height.</p>\n<h3>Heading (size 3)</h3>\n<h3>Heading (size 3) <a href=\"http://www.example.org\">with link</a></h3>\n<h3><strong>Heading</strong> (size 3)</h3>\n<p>Paragraph under heading to test line-height.</p>\n<p style=\"text-align: left;\"><strong>Bacon ipsum dolor amet short ribs shank cow, ribeye corned beef short loin t-bone kielbasa meatloaf ball tip rump venison boudin brisket beef ribs. Fatback landjaeger frankfurter, meatloaf picanha andouille leberkas.</strong> <em>Tail beef ribs boudin salami, kevin cupim landjaeger pork loin tenderloin ham filet mignon drumstick short loin. Biltong frankfurter shank pork belly picanha prosciutto meatloaf tail hamburger landjaeger pancetta shankle pig.</em> Pig tri-tip tenderloin ground round ribeye alcatra turkey salami turducken sausage pork loin kielbasa hamburger meatloaf strip steak.</p>\n<p style=\"text-align: left;\"><strong>Second paragraph, same text-alignment goes in the same table, other text alignments go into new tables, see?</strong> <em>Tail beef ribs boudin salami, kevin cupim landjaeger pork loin tenderloin ham filet mignon drumstick short loin. Ribeye boudin cow, beef ribs t-bone pig short ribs tri-tip pork loin rump shank hamburger short loin. Salami pastrami meatball shoulder cupim.</em></p>\n<p style=\"text-align: center;\">Bacon kevin shank ball tip shoulder. Jowl leberkas fatback, short loin chuck beef beef ribs short ribs ribeye turducken pork chop brisket filet mignon cow. Turkey ball tip rump bacon filet mignon sausage jowl shoulder chicken ground round kielbasa shankle. <em>Drumstick pancetta corned beef kielbasa porchetta jerky swine leberkas kevin boudin chicken shoulder bacon tri-tip venison.</em> Ham hock ball tip beef ribs spare ribs tail pork ground round, biltong doner t-bone pork chop rump hamburger pancetta brisket.</p>\n<p style=\"text-align: center;\"><strong>Second paragraph, same text-alignment goes in the same table, other text alignments go into new tables, see?</strong> <em>Tail beef ribs boudin salami, <span style=\"color: #ffff00;\">kevin cupim landjaeger pork loin tenderloin</span> ham filet mignon drumstick short loin. Ribeye boudin cow, beef ribs t-bone pig short ribs tri-tip pork loin rump shank hamburger short loin. Salami pastrami meatball shoulder cupim.</em></p>\n<p style=\"text-align: right;\">Brisket beef kielbasa jowl hamburger, doner flank. Shoulder ham hock sausage t-bone pork belly chicken picanha pork loin ham bresaola tri-tip ground round kevin. <em>Chicken sirloin shankle fatback boudin t-bone pig tri-tip bresaola doner cow short loin pancetta short ribs andouille. Cupim doner short ribs, andouille cow t-bone ground round pork porchetta beef capicola.</em> Rump drumstick biltong shank kielbasa bacon ball tip pancetta meatloaf shankle fatback.</p>\n<p style=\"text-align: right;\"><strong>Second paragraph, same text-alignment goes in the same table, other text alignments go into new tables, see?</strong> <em>Tail beef ribs boudin salami, kevin cupim landjaeger pork loin tenderloin ham filet mignon drumstick short loin. Ribeye boudin cow, beef ribs t-bone pig short ribs tri-tip pork loin rump shank hamburger short loin. Salami pastrami meatball shoulder cupim.</em></p>\n<p style=\"text-align: justify;\">Kielbasa jowl flank biltong. Pork loin fatback chicken ham prosciutto sausage cow short loin porchetta kielbasa. <em>Bresaola ham hock pancetta, cow ham tenderloin flank turducken fatback beef jowl short loin pig.</em> Picanha turkey spare ribs capicola andouille, tongue short loin sausage corned beef kevin meatball venison kielbasa pastrami. Beef ribs ground round tenderloin flank.</p>\n<p style=\"text-align: justify;\"><strong>Second paragraph, same text-alignment goes in the same table, other text alignments go into new tables, see?</strong> <em>Tail beef ribs boudin salami, kevin cupim landjaeger pork loin tenderloin ham filet mignon drumstick short loin. Ribeye boudin cow, beef ribs t-bone pig short ribs tri-tip pork loin rump shank hamburger short loin. Salami pastrami meatball shoulder cupim.</em> <a href=\"http://www.example.org\">Alcatra hamburger</a><br /><br /></p>\n<ul>\n<li>One</li>\n<li>Two</li>\n<li>Three</li>\n</ul>\n<ol>\n<li>One</li>\n<li>Two</li>\n<li>Three</li>\n</ol>\n<blockquote><h2>test header</h2>\n<p>Bacon ipsum dolor amet shoulder turkey meatball pork chop porchetta, filet mignon shankle. Sausage meatloaf flank picanha jowl chuck capicola tri-tip. Meatloaf andouille kielbasa beef ribs.</p>\n</blockquote>"
},
{
"type": "divider",

View File

@ -94,13 +94,13 @@ class ShortcodesTest extends MailPoetTest {
'<a data-post-id="10" href="#">another post</a>' .
'<a href="#">not post</a>';
$result =
$shortcodes_object->process(array('[newsletter:subject]'));
$shortcodes_object->process(array('[newsletter:subject]'), $content);
expect($result[0])->equals($this->newsletter->subject);
$result =
$shortcodes_object->process(array('[newsletter:total]'), $content);
expect($result[0])->equals(2);
$result =
$shortcodes_object->process(array('[newsletter:post_title]'));
$shortcodes_object->process(array('[newsletter:post_title]'), $content);
$wp_post = get_post($this->WP_post);
expect($result['0'])->equals($wp_post->post_title);
}
@ -269,6 +269,30 @@ class ShortcodesTest extends MailPoetTest {
}
}
function testItReturnsHashInsteadofLinksWhenInPreviewIsEnabled() {
$shortcodes_object = $this->shortcodes_object;
$shortcodes_object->wp_user_preview = true;
$shortcodes = array(
'[link:subscription_unsubscribe_url]',
'[link:subscription_manage_url]',
'[link:newsletter_view_in_browser_url]',
);
$result = $shortcodes_object->process($shortcodes);
// hash is returned
foreach($result as $index => $transformed_shortcode) {
expect($transformed_shortcode)->equals('#');
}
$shortcodes = array(
'[link:subscription_unsubscribe]',
'[link:subscription_manage]',
'[link:newsletter_view_in_browser]',
);
$result = $shortcodes_object->process($shortcodes);
foreach($result as $index => $transformed_shortcode) {
expect($transformed_shortcode)->regExp('/href="#"/');
}
}
function testItCanProcessCustomLinkShortcodes() {
$shortcodes_object = $this->shortcodes_object;
$shortcode = '[link:shortcode]';

View File

@ -1,17 +1,20 @@
<?php
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterLink;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Setting;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Links\Links;
use MailPoet\Newsletter\ViewInBrowser;
use MailPoet\Router\Router;
class ViewInBrowserTest extends MailPoetTest {
function __construct() {
$this->newsletter = array(
'body' => json_decode(
'{
$this->newsletter =
array(
'body' => json_decode(
'{
"content": {
"type": "container",
"orientation": "vertical",
@ -41,7 +44,7 @@ class ViewInBrowserTest extends MailPoetTest {
"blocks": [
{
"type": "text",
"text": "<p>Rendered newsletter. Hello,&nbsp;[subscriber:firstname | default:reader] & [link:newsletter_view_in_browser_url]</p>"
"text": "<p>Rendered newsletter. Hello, [subscriber:firstname | default:reader]. <a href=\"[link:newsletter_view_in_browser_url]\">Unsubscribe</a> or visit <a href=\"http://google.com\">Google</a></p>"
}
]
}
@ -50,22 +53,18 @@ class ViewInBrowserTest extends MailPoetTest {
]
}
}', true),
'id' => 1,
'subject' => 'Some subject',
'preheader' => 'Some preheader',
'type' => 'standard',
'status' => 'active'
);
'id' => 1,
'subject' => 'Some subject',
'preheader' => 'Some preheader',
'type' => 'standard',
'status' => 'active'
);
$this->queue_rendered_newsletter_without_tracking = array(
'html' => 'Newsletter from queue. Hello, [subscriber:firstname] &
[link:newsletter_view_in_browser_url]'
'html' => '<p>Newsletter from queue. Hello, [subscriber:firstname | default:reader]. <a href="[link:newsletter_view_in_browser_url]">Unsubscribe</a> or visit <a href="http://google.com">Google</a></p>'
);
$this->queue_rendered_newsletter_with_tracking = array(
'html' => 'Newsletter from queue. Hello, [subscriber:firstname] &
[mailpoet_click_data]-90e56'
'html' => '<p>Newsletter from queue. Hello, [subscriber:firstname | default:reader]. <a href="' . Links::DATA_TAG_CLICK . '-90e56">Unsubscribe</a> or visit <a href="' . Links::DATA_TAG_CLICK . '-i1893">Google</a><img alt="" class="" src="' . Links::DATA_TAG_OPEN . '"></p>'
);
// instantiate class
$this->view_in_browser = new ViewInBrowser();
}
function _before() {
@ -85,13 +84,19 @@ class ViewInBrowserTest extends MailPoetTest {
$queue->newsletter_rendered_body = $this->queue_rendered_newsletter_without_tracking;
$queue->subscribers = array('processed' => array($subscriber->id));
$this->queue = $queue->save();
// build browser preview data
$this->browser_preview_data = (object)array(
'queue' => $this->queue,
'subscriber' => $this->subscriber,
'newsletter' => $this->newsletter,
'preview' => false
);
// create newsletter link associations
$newsletter_link_1 = NewsletterLink::create();
$newsletter_link_1->hash = '90e56';
$newsletter_link_1->url = '[link:newsletter_view_in_browser_url]';
$newsletter_link_1->newsletter_id = $this->newsletter->id;
$newsletter_link_1->queue_id = $this->queue->id;
$this->newsletter_link_1 = $newsletter_link_1->save();
$newsletter_link_2 = NewsletterLink::create();
$newsletter_link_2->hash = 'i1893';
$newsletter_link_2->url = 'http://google.com';
$newsletter_link_2->newsletter_id = $this->newsletter->id;
$newsletter_link_2->queue_id = $this->queue->id;
$this->newsletter_link_2 = $newsletter_link_2->save();
}
function testItRendersNewsletter() {
@ -99,7 +104,7 @@ class ViewInBrowserTest extends MailPoetTest {
$this->newsletter,
$this->subscriber,
$queue = false,
$preview = true
$preview = false
);
expect($rendered_body)->regExp('/Rendered newsletter/');
}
@ -109,7 +114,7 @@ class ViewInBrowserTest extends MailPoetTest {
$this->newsletter,
$this->subscriber,
$this->queue,
$preview = true
$preview = false
);
expect($rendered_body)->regExp('/Newsletter from queue/');
}
@ -120,14 +125,26 @@ class ViewInBrowserTest extends MailPoetTest {
$this->newsletter,
$this->subscriber,
$this->queue,
$preview = true
$preview = false
);
expect($rendered_body)->contains('Hello, First');
expect($rendered_body)->contains(Router::NAME . '&endpoint=view_in_browser');
}
function testItProcessesLinksWhenTrackingIsEnabled() {
function testItRewritesLinksToRouterEndpointWhenTrackingIsEnabled() {
Setting::setValue('tracking.enabled', true);
$queue = $this->queue;
$queue->newsletter_rendered_body = $this->queue_rendered_newsletter_with_tracking;
$rendered_body = ViewInBrowser::renderNewsletter(
$this->newsletter,
$this->subscriber,
$queue,
$preview = false
);
expect($rendered_body)->contains(Router::NAME . '&endpoint=track');
}
function testItConvertsHashedLinksToUrlsWhenPreviewIsEnabledAndNewsletterWasSent() {
$queue = $this->queue;
$queue->newsletter_rendered_body = $this->queue_rendered_newsletter_with_tracking;
$rendered_body = ViewInBrowser::renderNewsletter(
@ -136,11 +153,42 @@ class ViewInBrowserTest extends MailPoetTest {
$queue,
$preview = true
);
expect($rendered_body)->contains(Router::NAME . '&endpoint=track');
// hashed link should be replaced with a URL
expect($rendered_body)->notContains('[mailpoet_click_data]');
expect($rendered_body)->contains('<a href="http://google.com">');
}
function testReplacesLinkShortcodesWithUrlHashWhenPreviewIsEnabledAndNewsletterWasSent() {
$queue = $this->queue;
$queue->newsletter_rendered_body = $this->queue_rendered_newsletter_with_tracking;
$rendered_body = ViewInBrowser::renderNewsletter(
$this->newsletter,
$this->subscriber,
$queue,
$preview = true
);
// link shortcodes should be replaced with a hash (#)
expect($rendered_body)->notContains('[mailpoet_click_data]');
expect($rendered_body)->contains('<a href="#">');
}
function testRemovesOpenTrackingTagWhenPreviewIsEnabledAndNewsletterWasSent() {
$queue = $this->queue;
$queue->newsletter_rendered_body = $this->queue_rendered_newsletter_with_tracking;
$rendered_body = ViewInBrowser::renderNewsletter(
$this->newsletter,
$this->subscriber,
$queue,
$preview = true
);
// open tracking data tag should be removed
expect($rendered_body)->notContains('[mailpoet_open_data]');
expect($rendered_body)->contains('<img alt="" class="" src="">');
}
function _after() {
ORM::raw_execute('TRUNCATE ' . Newsletter::$_table);
ORM::raw_execute('TRUNCATE ' . NewsletterLink::$_table);
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);
}

View File

@ -37,17 +37,13 @@ class ViewInBrowserRouterTest extends MailPoetTest {
function testItAbortsWhenBrowserPreviewDataIsMissing() {
$view_in_browser = Stub::make($this->view_in_browser, array(
'_abort' => Stub::exactly(3, function() { })
'_abort' => Stub::exactly(2, function() { })
), $this);
// newsletter ID is required
$data = $this->browser_preview_data;
unset($data['newsletter_id']);
$view_in_browser->_processBrowserPreviewData($data);
// subscriber ID is required
$data = $this->browser_preview_data;
unset($data['subscriber_id']);
$view_in_browser->_processBrowserPreviewData($data);
// subscriber token is required
// subscriber token is required if subscriber is provided
$data = $this->browser_preview_data;
unset($data['subscriber_token']);
$view_in_browser->_processBrowserPreviewData($data);
@ -61,43 +57,71 @@ class ViewInBrowserRouterTest extends MailPoetTest {
$data = $this->browser_preview_data;
$data['newsletter_id'] = 99;
$view_in_browser->_processBrowserPreviewData($data);
// subscriber ID is invalid
// subscriber token is invalid
$data = $this->browser_preview_data;
$data['subscriber_id'] = 99;
$data['subscriber_token'] = false;
$view_in_browser->_processBrowserPreviewData($data);
// subscriber token is invalid
$data = $this->browser_preview_data;
$data['subscriber_token'] = 'invalid';
$view_in_browser->_processBrowserPreviewData($data);
// subscriber has not received the newsletter
}
function testItFailsValidationWhenSubscriberTokenDoesNotMatch() {
$subscriber = $this->subscriber;
$subscriber->email = 'random@email.com';
$subscriber->save();
$data = (object)array_merge(
$this->browser_preview_data,
array(
'queue' => $this->queue,
'subscriber' => $this->subscriber,
'subscriber' => $subscriber,
'newsletter' => $this->newsletter
)
);
$data->subscriber->email = 'random@email.com';
expect($this->view_in_browser->_validateBrowserPreviewData($data))->false();
}
function testItFailsValidationWhenNewsletterIdIsProvidedButSubscriberDoesNotExist() {
$data = (object)$this->browser_preview_data;
$data->subscriber_id = false;
expect($this->view_in_browser->_validateBrowserPreviewData($data))->false();
}
function testItValidatesThatNewsletterExistsByCheckingHashFirst() {
$newsletter_1 = $this->newsletter;
$newsletter_2 = Newsletter::create();
$newsletter_2->type = 'type';
$newsletter_2 = $newsletter_2->save();
$data = (object)$this->browser_preview_data;
$data->newsletter_hash = $newsletter_2->hash;
$result = $this->view_in_browser->_validateBrowserPreviewData($data);
expect($result->newsletter->id)->equals($newsletter_2->id);
$data->newsletter_hash = false;
$result = $this->view_in_browser->_validateBrowserPreviewData($data);
expect($result->newsletter->id)->equals($newsletter_1->id);
}
function testItFailsValidationWhenPreviewIsEnabledButNewsletterHashNotProvided() {
$data = (object)$this->browser_preview_data;
$data->newsletter_hash = false;
$data->preview = true;
expect($this->view_in_browser->_validateBrowserPreviewData($data))->false();
}
function testItFailsValidationWhenSubscriberIsNotOnProcessedList() {
$data = (object)array_merge(
$this->browser_preview_data,
array(
'queue' => $this->queue,
'subscriber' => $this->subscriber,
'newsletter' => $this->newsletter
)
);
$data->subscriber->id = 99;
expect($this->view_in_browser->_validateBrowserPreviewData($data))->false();
$data = (object)$this->browser_preview_data;
$result = $this->view_in_browser->_validateBrowserPreviewData($data);
expect($result)->notEmpty();
$queue = $this->queue;
$queue->subscribers = array('processed' => array());
$queue->save();
$result = $this->view_in_browser->_validateBrowserPreviewData($data);
expect($result)->false();
}
function testItDoesNotRequireWpUsersToBeOnProcessedListWhenPreviewIsEnabled() {
function testItDoesNotRequireWpAdministratorToBeOnProcessedListWhenPreviewIsEnabled() {
$data = (object)array_merge(
$this->browser_preview_data,
array(
@ -106,8 +130,16 @@ class ViewInBrowserRouterTest extends MailPoetTest {
'newsletter' => $this->newsletter
)
);
$data->subscriber->wp_user_id = 99;
$data->preview = true;
// when WP user is not logged, false should be returned
expect($this->view_in_browser->_validateBrowserPreviewData($data))->false();
// when WP user is logged in but does not have 'manage options' permission, false should be returned
wp_set_current_user(1);
$wp_user = wp_get_current_user();
$wp_user->remove_role('administrator');
expect($this->view_in_browser->_validateBrowserPreviewData($data))->false();
// when WP user is logged and has 'manage options' permission, data should be returned
$wp_user->add_role('administrator');
expect($this->view_in_browser->_validateBrowserPreviewData($data))->equals($data);
}
@ -130,5 +162,8 @@ class ViewInBrowserRouterTest extends MailPoetTest {
ORM::raw_execute('TRUNCATE ' . Newsletter::$_table);
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);
// reset WP user role
$wp_user = wp_get_current_user();
$wp_user->add_role('administrator');
}
}

View File

@ -0,0 +1,54 @@
<?php
use \MailPoet\Twig\Assets;
class AssetsTest extends MailPoetTest {
function _before() {
$this->assets_url = 'https://www.testing.com/wp-content/plugins/mailpoet/assets';
$this->version = '1.2.3';
$this->assetsExtension = new Assets(array(
'assets_url' => $this->assets_url,
'version' => $this->version
));
}
function testItGeneratesJavascriptTags() {
expect($this->assetsExtension->generateJavascript('script1.js', 'script2.js'))->equals(
'<script type="text/javascript" src="' . $this->assets_url . '/js/script1.js?mailpoet_version=' . $this->version . '"></script>'
. "\n"
. '<script type="text/javascript" src="' . $this->assets_url . '/js/script2.js?mailpoet_version=' . $this->version . '"></script>'
);
}
function testItGeneratesStylesheetTags() {
expect($this->assetsExtension->generateStylesheet('style1.css', 'style2.css'))->equals(
'<link rel="stylesheet" type="text/css" href="' . $this->assets_url . '/css/style1.css?mailpoet_version=' . $this->version . '">'
. "\n"
. '<link rel="stylesheet" type="text/css" href="' . $this->assets_url . '/css/style2.css?mailpoet_version=' . $this->version . '">'
);
}
function testItGeneratesImageUrls() {
expect($this->assetsExtension->generateImageUrl('image1.png'))->equals(
$this->assets_url . '/img/image1.png?mailpoet_version=' . $this->version
);
}
function testItAppendsVersionToUrl() {
$without_file = 'http://url.com/';
expect($this->assetsExtension->appendVersionToUrl($without_file))->equals(
$without_file . '?mailpoet_version=' . $this->version
);
$with_file = 'http://url.com/file.php';
expect($this->assetsExtension->appendVersionToUrl($with_file))->equals(
$with_file . '?mailpoet_version=' . $this->version
);
$with_folder = 'http://url.com/folder/file.php';
expect($this->assetsExtension->appendVersionToUrl($with_folder))->equals(
$with_folder . '?mailpoet_version=' . $this->version
);
$with_query_string = 'http://url.com/folder/file.php?name=value';
expect($this->assetsExtension->appendVersionToUrl($with_query_string))->equals(
$with_query_string . '&mailpoet_version=' . $this->version
);
}
}

View File

@ -0,0 +1,28 @@
<?php
use \MailPoet\WP\Readme;
class ReadmeTest extends MailPoetTest {
function _before() {
// Sample taken from https://wordpress.org/plugins/about/readme.txt
$this->data = file_get_contents(dirname(__FILE__) . '/ReadmeTestData.txt');
}
function testItParsesChangelog() {
$result = Readme::parseChangelog($this->data);
expect(count($result))->equals(2);
expect(count($result[0]['changes']))->equals(2);
expect(count($result[1]['changes']))->equals(1);
}
function testItRespectsLimitOfParsedItems() {
$result = Readme::parseChangelog($this->data, 1);
expect(count($result))->equals(1);
}
function testItReturnsFalseOnMalformedData() {
$result = Readme::parseChangelog("");
expect($result)->false();
$result = Readme::parseChangelog("== Changelog ==\n\n\n=\n==");
expect($result)->false();
}
}

View File

@ -0,0 +1,116 @@
=== Plugin Name ===
Contributors: (this should be a list of wordpress.org userid's)
Donate link: http://example.com/
Tags: comments, spam
Requires at least: 4.6
Tested up to: 4.7
Stable tag: 4.3
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Here is a short description of the plugin. This should be no more than 150 characters. No markup here.
== Description ==
This is the long description. No limit, and you can use Markdown (as well as in the following sections).
For backwards compatibility, if this section is missing, the full length of the short description will be used, and
Markdown parsed.
A few notes about the sections above:
* "Contributors" is a comma separated list of wordpress.org usernames
* "Tags" is a comma separated list of tags that apply to the plugin
* "Requires at least" is the lowest version that the plugin will work on
* "Tested up to" is the highest version that you've *successfully used to test the plugin*. Note that it might work on
higher versions... this is just the highest one you've verified.
* Stable tag should indicate the Subversion "tag" of the latest stable version, or "trunk," if you use `/trunk/` for
stable.
Note that the `readme.txt` of the stable tag is the one that is considered the defining one for the plugin, so
if the `/trunk/readme.txt` file says that the stable tag is `4.3`, then it is `/tags/4.3/readme.txt` that'll be used
for displaying information about the plugin. In this situation, the only thing considered from the trunk `readme.txt`
is the stable tag pointer. Thus, if you develop in trunk, you can update the trunk `readme.txt` to reflect changes in
your in-development version, without having that information incorrectly disclosed about the current stable version
that lacks those changes -- as long as the trunk's `readme.txt` points to the correct stable tag.
If no stable tag is provided, it is assumed that trunk is stable, but you should specify "trunk" if that's where
you put the stable version, in order to eliminate any doubt.
== Installation ==
This section describes how to install the plugin and get it working.
e.g.
1. Upload the plugin files to the `/wp-content/plugins/plugin-name` directory, or install the plugin through the WordPress plugins screen directly.
1. Activate the plugin through the 'Plugins' screen in WordPress
1. Use the Settings->Plugin Name screen to configure the plugin
1. (Make your instructions match the desired user flow for activating and installing your plugin. Include any steps that might be needed for explanatory purposes)
== Frequently Asked Questions ==
= A question that someone might have =
An answer to that question.
= What about foo bar? =
Answer to foo bar dilemma.
== Screenshots ==
1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Note that the screenshot is taken from
the /assets directory or the directory that contains the stable readme.txt (tags or trunk). Screenshots in the /assets
directory take precedence. For example, `/assets/screenshot-1.png` would win over `/tags/4.3/screenshot-1.png`
(or jpg, jpeg, gif).
2. This is the second screen shot
== Changelog ==
= 1.0 =
* A change since the previous version.
* Another change.
= 0.5 =
* List versions from most recent at top to oldest at bottom.
== Upgrade Notice ==
= 1.0 =
Upgrade notices describe the reason a user should upgrade. No more than 300 characters.
= 0.5 =
This version fixes a security related bug. Upgrade immediately.
== Arbitrary section ==
You may provide arbitrary sections, in the same format as the ones above. This may be of use for extremely complicated
plugins where more information needs to be conveyed that doesn't fit into the categories of "description" or
"installation." Arbitrary sections will be shown below the built-in sections outlined above.
== A brief Markdown Example ==
Ordered list:
1. Some feature
1. Another feature
1. Something else about the plugin
Unordered list:
* something
* something else
* third thing
Here's a link to [WordPress](http://wordpress.org/ "Your favorite software") and one to [Markdown's Syntax Documentation][markdown syntax].
Titles are optional, naturally.
[markdown syntax]: http://daringfireball.net/projects/markdown/syntax
"Markdown is what the parser uses to process much of the readme file"
Markdown uses email style notation for blockquotes and I've been told:
> Asterisks for *emphasis*. Double it up for **strong**.
`<?php code(); // goes in backticks ?>`

View File

@ -146,7 +146,7 @@
%>
</p>
<p>
<%= __('%sHTML%s, %sPHP%s and %siFrame%s versions are also available.', 'wysija-newsletters')
<%= __('%sHTML%s, %sPHP%s and %siFrame%s versions are also available.')
| format(
'<a href="javascript:;" class="mailpoet_form_export_toggle" data-type="html">',
'</a>',
@ -711,4 +711,4 @@
<%= partial('mailpoet_form_preview_template',
'form/templates/preview.hbs'
) %>
<% endblock %>
<% endblock %>

View File

@ -241,4 +241,4 @@
'mailerResumeSendingButton': __('Resume sending'),
'mailerSendingResumedNotice': __('Sending has been resumed.')
}) %>
<% endblock %>
<% endblock %>

View File

@ -25,7 +25,7 @@
<p>{{{updated}}}</p>
{{/if}}
{{#if no_action}}
<p><%= __('No new subscribers were found/added') %></p>
<p><%= __('No subscribers were added or updated.') %></p>
{{/if}}
{{#if added_to_segment_with_welcome_notification}}
<p><%= __('Note: Imported subscribers will not receive any Welcome Emails') %></p>

View File

@ -23,30 +23,19 @@
<div id="mailpoet-changelog" clas="feature-section one-col">
<h2><%= __("List of Changes") %></h2>
<h3>3.0.0-beta.7.1 - 2016-12-06</h3>
<ul>
<li>Improved: allow user to restart sending after sending method failure;</li>
<li>Fixed: subscribers are not added to lists after import;</li>
<li>Fixed: sending should stop when newsletter is trashed;</li>
<li>Fixed: update database schema after an update which fixes an SQL error;</li>
<li>Fixed: status of sent newsletters is showing "paused" instead of "sent";</li>
<li>Fixed: dividers in Automatic Latest Posts posts are not displayed. Thx Gregor!;</li>
<li>Fixed: shortcodes (ie, first name) are not rendered when sending a preview;</li>
<li>Fixed: count of confirmed subscribers only in step 2 of import is erroneous.</li>
</ul>
<br>
<h3>3.0.0-beta.6 - 2016-11-29</h3>
<ul>
<li>Added: "bounced" status has been added to subscribers;</li>
<li>Improved: execution time enforced between individual send operations. Avoids duplicate sending on really slow servers;</li>
<li>Improved: Welcome emails are given higher priority for sending;</li>
<li>Fixed: Welcome emails are not scheduled for WP users;</li>
<li>Fixed: Unicode characters in FROM/REPLY-TO/TO fields are not rendered;</li>
<li>Fixed: sending HTML emails with Amazon SES works again. Kudos Alex for reporting;</li>
<li>Fixed: import fails when subscriber already exists in the database but the email is in different case format. Thx Ellen for telling us;</li>
<li>Fixed: ampersand char ("&") inside the subject line won't throw errors in browser preview. Thanks Michel for reporting.</li>
</ul>
<br>
<% if changelog %>
<% for item in changelog %>
<h3><%= item.version %></h3>
<ul>
<% for change in item.changes %>
<li><%= change %></li>
<% endfor %>
</ul>
<br>
<% endfor %>
<% else %>
<p style="text-align: center"><%= __("See readme.txt for a changelog.") %></p>
<% endif %>
</div>
<hr>