Compare commits
27 Commits
3.0.0-beta
...
3.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
0aa48b9121 | |||
1157cc8b9a | |||
88599963e0 | |||
57706dc1b3 | |||
694402e9f2 | |||
891239bf4e | |||
232494e1a2 | |||
4ae55230da | |||
eda4a9edcc | |||
5fb699fd5b | |||
7c5e0212ad | |||
44a77e097b | |||
22fd9e31f7 | |||
bbe3d48ec1 | |||
449978d7c1 | |||
0535e1eaeb | |||
4dfe4e4997 | |||
33a184fc4a | |||
19b34ed838 | |||
ca17e0c4da | |||
347e491865 | |||
59b6877675 | |||
6728203672 | |||
8a1450d7d6 | |||
49f2b147be | |||
8f3b2e6c0b | |||
baf0d374ae |
8
.circle_ci/fake-sendmail.rb
Executable file
8
.circle_ci/fake-sendmail.rb
Executable 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
|
3
.circle_ci/mailpoet_php.ini
Normal file
3
.circle_ci/mailpoet_php.ini
Normal 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
|
@ -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
|
||||
|
@ -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,12 @@ 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 NAMES utf8',
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND =>
|
||||
'SET TIME_ZONE = "' . Env::$db_timezone_offset. '"',
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND =>
|
||||
'SET sql_mode=(SELECT REPLACE(@@sql_mode,"ONLY_FULL_GROUP_BY",""))'
|
||||
));
|
||||
|
||||
$settings = Env::$db_prefix . 'settings';
|
||||
@ -128,6 +132,7 @@ class Initializer {
|
||||
$this->setupAPI();
|
||||
$this->setupRouter();
|
||||
$this->setupPages();
|
||||
do_action('mailpoet_initialized', MAILPOET_VERSION);
|
||||
} catch(\Exception $e) {
|
||||
$this->handleFailedInitialization($e);
|
||||
}
|
||||
@ -223,4 +228,4 @@ class Initializer {
|
||||
function handleFailedInitialization($message) {
|
||||
return WPNotice::displayError($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,',
|
||||
|
@ -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
|
||||
)));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
197
lib/Cron/Workers/Bounce.php
Normal 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);
|
||||
}
|
||||
}
|
41
lib/Cron/Workers/Bounce/API.php
Normal file
41
lib/Cron/Workers/Bounce/API.php
Normal 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) === 201) {
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -167,6 +167,7 @@ class SendingQueue {
|
||||
return SendingQueueModel::orderByAsc('priority')
|
||||
->whereNull('deleted_at')
|
||||
->whereNull('status')
|
||||
->whereNull('type')
|
||||
->findMany();
|
||||
}
|
||||
}
|
@ -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) . '
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
namespace MailPoet\Util;
|
||||
use \Sunra\PhpSimple\HtmlDomParser;
|
||||
use csstidy;
|
||||
|
||||
/*
|
||||
|
49
lib/WP/Readme.php
Normal file
49
lib/WP/Readme.php
Normal 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;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ use MailPoet\Config\Initializer;
|
||||
|
||||
/*
|
||||
* Plugin Name: MailPoet
|
||||
* Version: 3.0.0-beta.7.1
|
||||
* Version: 3.0.0-beta.8
|
||||
* 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
|
||||
@ -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.8');
|
||||
$initializer = new Initializer(
|
||||
array(
|
||||
'file' => __FILE__,
|
||||
|
@ -3,7 +3,7 @@ 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
|
||||
Stable tag: 3.0.0-beta.8
|
||||
Create and send beautiful emails and newsletters from WordPress.
|
||||
|
||||
== Description ==
|
||||
@ -83,6 +83,13 @@ Our [support site](https://docs.mailpoet.com/) has plenty of articles. You can w
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 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;
|
||||
|
208
tests/unit/Cron/Workers/BounceTest.php
Normal file
208
tests/unit/Cron/Workers/BounceTest.php
Normal 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);
|
||||
}
|
||||
}
|
18
tests/unit/Cron/Workers/BounceTestMockAPI.php
Normal file
18
tests/unit/Cron/Workers/BounceTestMockAPI.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
54
tests/unit/Twig/AssetsTest.php
Normal file
54
tests/unit/Twig/AssetsTest.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
28
tests/unit/WP/ReadmeTest.php
Normal file
28
tests/unit/WP/ReadmeTest.php
Normal 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();
|
||||
}
|
||||
}
|
116
tests/unit/WP/ReadmeTestData.txt
Normal file
116
tests/unit/WP/ReadmeTestData.txt
Normal 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 ?>`
|
@ -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>
|
||||
|
Reference in New Issue
Block a user