Merge pull request #564 from mailpoet/cron_update

Cron update
This commit is contained in:
Tautvidas Sipavičius
2016-08-08 16:59:27 +03:00
committed by GitHub
23 changed files with 366 additions and 309 deletions

View File

@ -21,8 +21,6 @@ define(
action: 'getStatus' action: 'getStatus'
}) })
.done(function(response) { .done(function(response) {
jQuery('.button-primary')
.removeClass('disabled');
if(response.status !== undefined) { if(response.status !== undefined) {
this.setState(response); this.setState(response);
} else { } else {
@ -36,55 +34,26 @@ define(
setInterval(this.getCronData, 5000); setInterval(this.getCronData, 5000);
} }
}, },
controlCron: function(action) {
if(jQuery('.button-primary').hasClass('disabled')) {
return;
}
jQuery('.button-primary')
.addClass('disabled');
MailPoet.Ajax.post({
endpoint: 'cron',
action: action,
})
.done(function(response) {
if(!response.result) {
MailPoet.Notice.error(MailPoet.I18n.t('daemonControlError'));
}
}.bind(this));
},
render: function() { render: function() {
if(this.state.status === 'loading') {
return(<div>{MailPoet.I18n.t('loadingDaemonStatus')}</div>);
}
switch(this.state.status) { switch(this.state.status) {
case 'started': case 'loading':
return( return(
<div> <div>
{MailPoet.I18n.t('cronDaemonIsRunning')} {MailPoet.I18n.t('loadingDaemonStatus')}
<br/>
<br/>
<a href="#" className="button-primary" onClick={this.controlCron.bind(null, 'stop')}>{MailPoet.I18n.t('stop')}</a>
</div> </div>
); );
break; case false:
case 'starting': return(
case 'stopping': <div>
{MailPoet.I18n.t('daemonNotRunning')}
</div>
);
default:
return( return(
<div> <div>
{MailPoet.I18n.t('cronDaemonState').replace('%$1s', this.state.status)} {MailPoet.I18n.t('cronDaemonState').replace('%$1s', this.state.status)}
</div> </div>
); );
break;
case 'stopped':
return(
<div>
{MailPoet.I18n.t('cronDaemonState').replace('%$1s', this.state.status)}
<br />
<br />
<a href="#" className="button-primary" onClick={this.controlCron.bind(null, 'start')}>{MailPoet.I18n.t('start')}</a>
</div>
);
break;
} }
} }
}); });

View File

@ -2,35 +2,15 @@
namespace MailPoet\API\Endpoints; namespace MailPoet\API\Endpoints;
use MailPoet\Cron\CronHelper; use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Supervisor;
use MailPoet\Models\Setting; use MailPoet\Models\Setting;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class Cron { class Cron {
function start() {
$supervisor = new Supervisor($force_run = true);
return $supervisor->checkDaemon();
}
function stop() {
$daemon = CronHelper::getDaemon();
if(!$daemon || $daemon['status'] !== 'started') {
$result = false;
} else {
$daemon['status'] = 'stopping';
$result = CronHelper::saveDaemon($daemon);
}
return array(
'result' => $result
);
}
function getStatus() { function getStatus() {
$daemon = Setting::where('name', 'cron_daemon') $daemon = Setting::getValue(CronHelper::DAEMON_SETTING);
->findOne();
return ($daemon) ? return ($daemon) ?
unserialize($daemon->value) : $daemon :
false; array('status' => false);
} }
} }

View File

@ -2,7 +2,7 @@
namespace MailPoet\Config; namespace MailPoet\Config;
use MailPoet\Models; use MailPoet\Models;
use MailPoet\Cron\Supervisor; use MailPoet\Cron\CronTrigger;
use MailPoet\Router; use MailPoet\Router;
use MailPoet\API; use MailPoet\API;
use MailPoet\WP\Notice as WPNotice; use MailPoet\WP\Notice as WPNotice;
@ -107,7 +107,7 @@ class Initializer {
$this->setupShortcodes(); $this->setupShortcodes();
$this->setupHooks(); $this->setupHooks();
$this->setupImages(); $this->setupImages();
$this->runQueueSupervisor(); $this->setupCronTrigger();
$this->plugin_initialized = true; $this->plugin_initialized = true;
} catch(\Exception $e) { } catch(\Exception $e) {
@ -192,14 +192,9 @@ class Initializer {
$router->init(); $router->init();
} }
function runQueueSupervisor() { function setupCronTrigger() {
if(php_sapi_name() === 'cli') return; $cron_trigger = new CronTrigger();
try { $cron_trigger->init();
$supervisor = new Supervisor();
$supervisor->checkDaemon();
} catch(\Exception $e) {
// Prevent Daemon exceptions from breaking out and breaking UI
}
} }
function setupImages() { function setupImages() {

View File

@ -1,6 +1,7 @@
<?php <?php
namespace MailPoet\Config; namespace MailPoet\Config;
use MailPoet\Cron\CronTrigger;
use MailPoet\Form\Block; use MailPoet\Form\Block;
use MailPoet\Form\Renderer as FormRenderer; use MailPoet\Form\Renderer as FormRenderer;
use MailPoet\Models\CustomField; use MailPoet\Models\CustomField;
@ -255,6 +256,7 @@ class Menu {
$data = array( $data = array(
'settings' => $settings, 'settings' => $settings,
'segments' => Segment::getPublic()->findArray(), 'segments' => Segment::getPublic()->findArray(),
'cron_trigger' => CronTrigger::getAvailableMethods(),
'pages' => Pages::getAll(), 'pages' => Pages::getAll(),
'flags' => $flags, 'flags' => $flags,
'current_user' => wp_get_current_user(), 'current_user' => wp_get_current_user(),

View File

@ -10,6 +10,7 @@ use MailPoet\Config\PopulatorData\Templates\PostNotificationsBlank1Column;
use MailPoet\Config\PopulatorData\Templates\WelcomeBlank1Column; use MailPoet\Config\PopulatorData\Templates\WelcomeBlank1Column;
use MailPoet\Config\PopulatorData\Templates\WelcomeBlank12Column; use MailPoet\Config\PopulatorData\Templates\WelcomeBlank12Column;
use MailPoet\Config\PopulatorData\Templates\SimpleText; use MailPoet\Config\PopulatorData\Templates\SimpleText;
use MailPoet\Cron\CronTrigger;
use \MailPoet\Models\Segment; use \MailPoet\Models\Segment;
use \MailPoet\Segments\WP; use \MailPoet\Segments\WP;
use \MailPoet\Models\Setting; use \MailPoet\Models\Setting;
@ -74,6 +75,13 @@ class Populator {
private function createDefaultSettings() { private function createDefaultSettings() {
$current_user = wp_get_current_user(); $current_user = wp_get_current_user();
if(!Setting::getValue(CronTrigger::SETTING_NAME)) {
// disable task scheduler (cron) be default
Setting::setValue(CronTrigger::SETTING_NAME, array(
'method' => CronTrigger::DEFAULT_METHOD
));
}
// default sender info based on current user // default sender info based on current user
$sender = array( $sender = array(
'name' => $current_user->display_name, 'name' => $current_user->display_name,

View File

@ -1,9 +1,9 @@
<?php <?php
namespace MailPoet\Cron; namespace MailPoet\Cron;
use MailPoet\Router\Front as FrontRouter;
use MailPoet\Router\Endpoints\Queue as QueueEndpoint;
use MailPoet\Models\Setting; use MailPoet\Models\Setting;
use MailPoet\Router\Endpoints\Queue as QueueEndpoint;
use MailPoet\Router\Front as FrontRouter;
use MailPoet\Util\Security; use MailPoet\Util\Security;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
@ -12,6 +12,7 @@ class CronHelper {
const DAEMON_EXECUTION_LIMIT = 20; const DAEMON_EXECUTION_LIMIT = 20;
const DAEMON_EXECUTION_TIMEOUT = 35; const DAEMON_EXECUTION_TIMEOUT = 35;
const DAEMON_REQUEST_TIMEOUT = 2; const DAEMON_REQUEST_TIMEOUT = 2;
const DAEMON_SETTING = 'cron_daemon';
static function createDaemon($token) { static function createDaemon($token) {
$daemon = array( $daemon = array(
@ -22,18 +23,32 @@ class CronHelper {
return $daemon; return $daemon;
} }
static function restartDaemon($token) {
return self::createDaemon($token);
}
static function getDaemon() { static function getDaemon() {
return Setting::getValue('cron_daemon'); return Setting::getValue(self::DAEMON_SETTING);
} }
static function saveDaemon($daemon) { static function saveDaemon($daemon) {
$daemon['updated_at'] = time(); $daemon['updated_at'] = time();
return Setting::setValue( return Setting::setValue(
'cron_daemon', self::DAEMON_SETTING,
$daemon $daemon
); );
} }
static function stopDaemon() {
$daemon = self::getDaemon();
$daemon['status'] = Daemon::STATUS_STOPPED;
return self::saveDaemon($daemon);
}
static function deleteDaemon() {
return Setting::deleteValue(self::DAEMON_SETTING);
}
static function createToken() { static function createToken() {
return Security::generateRandomString(); return Security::generateRandomString();
} }
@ -46,6 +61,8 @@ class CronHelper {
$data $data
); );
$args = array( $args = array(
'blocking' => false,
'sslverify' => false,
'timeout' => $timeout, 'timeout' => $timeout,
'user-agent' => 'MailPoet (www.mailpoet.com) Cron' 'user-agent' => 'MailPoet (www.mailpoet.com) Cron'
); );
@ -70,7 +87,7 @@ class CronHelper {
throw new \Exception(__('Site URL is unreachable.')); throw new \Exception(__('Site URL is unreachable.'));
} }
static function checkExecutionTimer($timer) { static function enforceExecutionLimit($timer) {
$elapsed_time = microtime(true) - $timer; $elapsed_time = microtime(true) - $timer;
if($elapsed_time >= self::DAEMON_EXECUTION_LIMIT) { if($elapsed_time >= self::DAEMON_EXECUTION_LIMIT) {
throw new \Exception(__('Maximum execution time has been reached.')); throw new \Exception(__('Maximum execution time has been reached.'));

41
lib/Cron/CronTrigger.php Normal file
View File

@ -0,0 +1,41 @@
<?php
namespace MailPoet\Cron;
use MailPoet\Models\Setting;
if(!defined('ABSPATH')) exit;
class CronTrigger {
public $current_method;
public static $available_methods = array(
'mailpoet' => 'MailPoet',
'wordpress' => 'WordPress'
);
const DEFAULT_METHOD = 'WordPress';
const SETTING_NAME = 'cron_trigger';
function __construct() {
$this->current_method = self::getCurrentMethod();
}
function init() {
try {
// configure cron trigger only outside of cli environment
if(php_sapi_name() === 'cli') return;
$trigger_class = __NAMESPACE__ . '\Triggers\\' . $this->current_method;
return (class_exists($trigger_class)) ?
$trigger_class::run() :
false;
} catch(\Exception $e) {
// cron exceptions should not prevent the rest of the site from loading
}
}
static function getAvailableMethods() {
return self::$available_methods;
}
static function getCurrentMethod() {
return Setting::getValue(self::SETTING_NAME . '.method');
}
}

View File

@ -78,15 +78,16 @@ class Daemon {
} }
} }
function abortWithError($message) {
exit('[mailpoet_cron_error:' . base64_encode($message) . ']');
}
function callSelf() { function callSelf() {
CronHelper::accessDaemon($this->token, self::REQUEST_TIMEOUT); CronHelper::accessDaemon($this->token, self::REQUEST_TIMEOUT);
$this->terminateRequest(); $this->terminateRequest();
} }
function abortWithError($message) {
status_header(404, $message);
exit;
}
function terminateRequest() { function terminateRequest() {
exit; exit;
} }

View File

@ -6,87 +6,35 @@ if(!defined('ABSPATH')) exit;
class Supervisor { class Supervisor {
public $daemon; public $daemon;
public $token; public $token;
public $force_run;
function __construct($force_run = false) { function __construct() {
$this->daemon = CronHelper::getDaemon();
$this->token = CronHelper::createToken(); $this->token = CronHelper::createToken();
$this->force_run = $force_run; $this->daemon = $this->getDaemon();
} }
function checkDaemon() { function checkDaemon() {
$daemon = $this->daemon; $daemon = $this->daemon;
if(!$daemon) { $execution_timeout_exceeded =
$daemon = CronHelper::createDaemon($this->token); (time() - (int)$daemon['updated_at']) > CronHelper::DAEMON_EXECUTION_TIMEOUT;
return $this->runDaemon($daemon); if($execution_timeout_exceeded) {
} CronHelper::restartDaemon($this->token);
// if the daemon is stopped, return its status and do nothing
if(!$this->force_run &&
isset($daemon['status']) &&
$daemon['status'] === Daemon::STATUS_STOPPED
) {
return $this->formatDaemonStatusMessage($daemon['status']);
}
$elapsed_time = time() - (int)$daemon['updated_at'];
// if it's been less than 40 seconds since last execution and we're not
// force-running the daemon, return its status and do nothing
if($elapsed_time < CronHelper::DAEMON_EXECUTION_TIMEOUT && !$this->force_run) {
return $this->formatDaemonStatusMessage($daemon['status']);
} elseif($elapsed_time < CronHelper::DAEMON_EXECUTION_TIMEOUT &&
$this->force_run &&
in_array($daemon['status'], array(
Daemon::STATUS_STOPPING,
Daemon::STATUS_STARTING
))
) {
// if it's been less than 40 seconds since last execution, we are
// force-running the daemon and it's either being started or stopped,
// return its status and do nothing
return $this->formatDaemonStatusMessage($daemon['status']);
}
// re-create (restart) daemon
CronHelper::createDaemon($this->token);
return $this->runDaemon(); return $this->runDaemon();
} }
return $daemon;
}
function runDaemon() { function runDaemon() {
$request = CronHelper::accessDaemon($this->token); CronHelper::accessDaemon($this->token);
preg_match('/\[(mailpoet_cron_error:.*?)\]/i', $request, $status); $daemon = CronHelper::getDaemon();
return $daemon;
}
function getDaemon() {
$daemon = CronHelper::getDaemon(); $daemon = CronHelper::getDaemon();
if(!empty($status) || !$daemon) {
if(!$daemon) { if(!$daemon) {
$message = __('Daemon failed to run.'); CronHelper::createDaemon($this->token);
} else { return $this->runDaemon();
list(, $message) = explode(':', $status[0]);
$message = base64_decode($message);
} }
return $this->formatResultMessage( return $daemon;
false,
$message
);
}
return $this->formatDaemonStatusMessage($daemon['status']);
}
private function formatDaemonStatusMessage($status) {
return $this->formatResultMessage(
true,
sprintf(
__('Daemon is currently %s.'),
__($status)
)
);
}
private function formatResultMessage($result, $message) {
$formattedResult = array(
'result' => $result
);
if(!$result) {
$formattedResult['errors'] = array($message);
} else {
$formattedResult['message'] = $message;
}
return $formattedResult;
} }
} }

View File

@ -0,0 +1,13 @@
<?php
namespace MailPoet\Cron\Triggers;
use MailPoet\Cron\Supervisor;
if(!defined('ABSPATH')) exit;
class MailPoet {
static function run() {
$supervisor = new Supervisor();
return $supervisor->checkDaemon();
}
}

View File

@ -0,0 +1,31 @@
<?php
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\Mailer\MailerLog;
if(!defined('ABSPATH')) exit;
class WordPress {
static function run() {
return (self::checkExecutionRequirements()) ?
MailPoet::run() :
self::cleanup();
}
static function checkExecutionRequirements() {
$scheduled_queues = SchedulerWorker::getScheduledQueues();
$running_queues = SendingQueueWorker::getRunningQueues();
$sending_limit_reached = MailerLog::isSendingLimitReached();
return (($scheduled_queues || $running_queues) && !$sending_limit_reached);
}
static function cleanup() {
$cron_daemon = CronHelper::getDaemon();
if($cron_daemon) {
CronHelper::deleteDaemon();
}
}
}

View File

@ -20,13 +20,12 @@ class Scheduler {
function __construct($timer = false) { function __construct($timer = false) {
$this->timer = ($timer) ? $timer : microtime(true); $this->timer = ($timer) ? $timer : microtime(true);
CronHelper::checkExecutionTimer($this->timer); // abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
} }
function process() { function process() {
$scheduled_queues = SendingQueue::where('status', 'scheduled') $scheduled_queues = self::getScheduledQueues();
->whereLte('scheduled_at', Carbon::createFromTimestamp(current_time('timestamp')))
->findMany();
if(!count($scheduled_queues)) return; if(!count($scheduled_queues)) return;
foreach($scheduled_queues as $i => $queue) { foreach($scheduled_queues as $i => $queue) {
$newsletter = Newsletter::filter('filterWithOptions') $newsletter = Newsletter::filter('filterWithOptions')
@ -40,7 +39,7 @@ class Scheduler {
} elseif($newsletter->type === 'standard') { } elseif($newsletter->type === 'standard') {
$this->processScheduledStandardNewsletter($newsletter, $queue); $this->processScheduledStandardNewsletter($newsletter, $queue);
} }
CronHelper::checkExecutionTimer($this->timer); CronHelper::enforceExecutionLimit($this->timer);
} }
} }
@ -185,4 +184,10 @@ class Scheduler {
$notification_history : $notification_history :
false; false;
} }
static function getScheduledQueues() {
return SendingQueue::where('status', 'scheduled')
->whereLte('scheduled_at', Carbon::createFromTimestamp(current_time('timestamp')))
->findMany();
}
} }

View File

@ -5,7 +5,7 @@ use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask; use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask; use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Subscribers as SubscribersTask; use MailPoet\Cron\Workers\SendingQueue\Tasks\Subscribers as SubscribersTask;
use MailPoet\Models\Newsletter as NewsletterModel; use MailPoet\Mailer\MailerLog;
use MailPoet\Models\SendingQueue as SendingQueueModel; use MailPoet\Models\SendingQueue as SendingQueueModel;
use MailPoet\Models\StatisticsNewsletters as StatisticsNewslettersModel; use MailPoet\Models\StatisticsNewsletters as StatisticsNewslettersModel;
use MailPoet\Models\Subscriber as SubscriberModel; use MailPoet\Models\Subscriber as SubscriberModel;
@ -23,11 +23,14 @@ class SendingQueue {
$this->mailer_task = new MailerTask(); $this->mailer_task = new MailerTask();
$this->newsletter_task = new NewsletterTask(); $this->newsletter_task = new NewsletterTask();
$this->timer = ($timer) ? $timer : microtime(true); $this->timer = ($timer) ? $timer : microtime(true);
// abort if execution or sending limit are reached
CronHelper::enforceExecutionLimit($this->timer);
} }
function process() { function process() {
$this->mailer_task->checkSendingLimit(); foreach(self::getRunningQueues() as $queue) {
foreach($this->getQueues() as $queue) { // abort if sending limit is reached
MailerLog::enforceSendingLimit();
// get and pre-process newsletter (render, replace shortcodes/links, etc.) // get and pre-process newsletter (render, replace shortcodes/links, etc.)
$newsletter = $this->newsletter_task->getAndPreProcess($queue->asArray()); $newsletter = $this->newsletter_task->getAndPreProcess($queue->asArray());
if(!$newsletter) { if(!$newsletter) {
@ -67,6 +70,11 @@ class SendingQueue {
$newsletter, $newsletter,
$found_subscribers $found_subscribers
); );
if($queue->status === SendingQueueModel::STATUS_COMPLETED) {
$this->newsletter_task->markNewsletterAsSent($queue->newsletter_id);
}
// abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
} }
} }
} }
@ -141,6 +149,7 @@ class SendingQueue {
$prepared_subscribers_ids, $prepared_subscribers_ids,
$queue->subscribers $queue->subscribers
); );
$queue = $this->updateQueue($queue);
} else { } else {
// update processed/to process list // update processed/to process list
$queue->subscribers = SubscribersTask::updateProcessedList( $queue->subscribers = SubscribersTask::updateProcessedList(
@ -149,19 +158,18 @@ class SendingQueue {
); );
// log statistics // log statistics
StatisticsNewslettersModel::createMultiple($statistics); StatisticsNewslettersModel::createMultiple($statistics);
// keep track of sent items // update the sent count
$this->mailer_task->updateMailerLog(); $this->mailer_task->updateSentCount();
$subscribers_to_process_count = count($queue->subscribers['to_process']);
}
$queue = $this->updateQueue($queue); $queue = $this->updateQueue($queue);
if($subscribers_to_process_count) { // enforce sending limit if there are still subscribers left to process
$this->mailer_task->checkSendingLimit(); if($queue->count_to_process) {
MailerLog::enforceSendingLimit();
}
} }
CronHelper::checkExecutionTimer($this->timer);
return $queue; return $queue;
} }
function getQueues() { static function getRunningQueues() {
return SendingQueueModel::orderByDesc('priority') return SendingQueueModel::orderByDesc('priority')
->whereNull('deleted_at') ->whereNull('deleted_at')
->whereNull('status') ->whereNull('status')
@ -178,13 +186,6 @@ class SendingQueue {
if(!$queue->count_to_process) { if(!$queue->count_to_process) {
$queue->processed_at = current_time('mysql'); $queue->processed_at = current_time('mysql');
$queue->status = SendingQueueModel::STATUS_COMPLETED; $queue->status = SendingQueueModel::STATUS_COMPLETED;
// if it's a standard or post notificaiton newsletter, update its status to sent
$newsletter = NewsletterModel::findOne($queue->newsletter_id);
if($newsletter->type === NewsletterModel::TYPE_STANDARD ||
$newsletter->type === NewsletterModel::TYPE_NOTIFICATION_HISTORY
) {
$newsletter->setStatus(NewsletterModel::STATUS_SENT);
}
} }
return $queue->save(); return $queue->save();
} }

View File

@ -2,18 +2,14 @@
namespace MailPoet\Cron\Workers\SendingQueue\Tasks; namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
use MailPoet\Mailer\Mailer as MailerFactory; use MailPoet\Mailer\Mailer as MailerFactory;
use MailPoet\Models\Setting; use MailPoet\Mailer\MailerLog;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class Mailer { class Mailer {
public $mta_config;
public $mta_log;
public $mailer; public $mailer;
function __construct() { function __construct() {
$this->mta_config = $this->getMailerConfig();
$this->mta_log = $this->getMailerLog();
$this->mailer = $this->configureMailer(); $this->mailer = $this->configureMailer();
} }
@ -40,33 +36,16 @@ class Mailer {
return $this->mailer; return $this->mailer;
} }
function getMailerConfig() {
$mta_config = Setting::getValue('mta');
if(!$mta_config) {
throw new \Exception(__('Mailer is not configured'));
}
return $mta_config;
}
function getMailerLog() { function getMailerLog() {
$mta_log = Setting::getValue('mta_log'); return MailerLog::getMailerLog();
if(!$mta_log) {
$mta_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue('mta_log', $mta_log);
}
return $mta_log;
} }
function updateMailerLog() { function updateSentCount() {
$this->mta_log['sent']++; return MailerLog::incrementSentCount();
Setting::setValue('mta_log', $this->mta_log);
} }
function getProcessingMethod() { function getProcessingMethod() {
return ($this->mta_config['method'] === 'MailPoet') ? return ($this->mailer->mailer_config['method'] === MailerFactory::METHOD_MAILPOET) ?
'bulk' : 'bulk' :
'individual'; 'individual';
} }
@ -81,23 +60,4 @@ class Mailer {
$prepared_subscribers $prepared_subscribers
); );
} }
function checkSendingLimit() {
if($this->mta_config['method'] === 'MailPoet') return;
$frequency_interval = (int)$this->mta_config['frequency']['interval'] * 60;
$frequency_limit = (int)$this->mta_config['frequency']['emails'];
$elapsed_time = time() - (int)$this->mta_log['started'];
if($this->mta_log['sent'] === $frequency_limit &&
$elapsed_time <= $frequency_interval
) {
throw new \Exception(__('Sending frequency limit has been reached'));
}
if($elapsed_time > $frequency_interval) {
$this->mta_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue('mta_log', $this->mta_log);
}
}
} }

View File

@ -62,7 +62,7 @@ class Newsletter {
return $newsletter; return $newsletter;
} }
function render($newsletter) { function render(array $newsletter) {
$renderer = new Renderer($newsletter); $renderer = new Renderer($newsletter);
$newsletter['rendered_body'] = $renderer->render(); $newsletter['rendered_body'] = $renderer->render();
return $newsletter; return $newsletter;
@ -103,4 +103,12 @@ class Newsletter {
) )
); );
} }
function markNewsletterAsSent($newsletter_id) {
$newsletter = NewsletterModel::findOne($newsletter_id);
// if it's a standard newsletter, update its status
if($newsletter->type === NewsletterModel::TYPE_STANDARD) {
$newsletter->setStatus(NewsletterModel::STATUS_SENT);
}
}
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace MailPoet\Cron\Workers\SendingQueue\Tasks; namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
use MailPoet\Models\Newsletter; use MailPoet\Models\Newsletter as NewsletterModel;
use MailPoet\Models\NewsletterPost; use MailPoet\Models\NewsletterPost;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
@ -19,7 +19,7 @@ class Posts {
if(!count($matched_posts_ids)) { if(!count($matched_posts_ids)) {
return $newsletter; return $newsletter;
} }
$newsletter_id = ($newsletter['type'] === Newsletter::TYPE_NOTIFICATION_HISTORY) ? $newsletter_id = ($newsletter['type'] === NewsletterModel::TYPE_NOTIFICATION_HISTORY) ?
$newsletter['parent_id'] : $newsletter['parent_id'] :
$newsletter['id']; $newsletter['id'];
foreach($matched_posts_ids as $post_id) { foreach($matched_posts_ids as $post_id) {

View File

@ -8,13 +8,22 @@ require_once(ABSPATH . 'wp-includes/pluggable.php');
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class Mailer { class Mailer {
public $mailer; public $mailer_config;
public $sender; public $sender;
public $reply_to; public $reply_to;
public $mailer_instance; public $mailer_instance;
const MAILER_CONFIG = 'mta';
const SENDING_LIMIT_INTERVAL_MULTIPLIER = 60;
const METHOD_MAILPOET = 'MailPoet';
const METHOD_MAILGUN = 'MailGun';
const METHOD_ELASTICEMAIL = 'ElasticEmail';
const METHOD_AMAZONSES = 'AmazonSES';
const METHOD_SENDGRID = 'SendGrid';
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) {
$this->mailer = $this->getMailer($mailer); $this->mailer_config = self::getMailerConfig($mailer);
$this->sender = $this->getSender($sender); $this->sender = $this->getSender($sender);
$this->reply_to = $this->getReplyTo($reply_to); $this->reply_to = $this->getReplyTo($reply_to);
$this->mailer_instance = $this->buildMailer(); $this->mailer_instance = $this->buildMailer();
@ -26,59 +35,59 @@ class Mailer {
} }
function buildMailer() { function buildMailer() {
switch($this->mailer['method']) { switch($this->mailer_config['method']) {
case 'AmazonSES': case self::METHOD_AMAZONSES:
$mailer_instance = new $this->mailer['class']( $mailer_instance = new $this->mailer_config['class'](
$this->mailer['region'], $this->mailer_config['region'],
$this->mailer['access_key'], $this->mailer_config['access_key'],
$this->mailer['secret_key'], $this->mailer_config['secret_key'],
$this->sender, $this->sender,
$this->reply_to $this->reply_to
); );
break; break;
case 'ElasticEmail': case self::METHOD_ELASTICEMAIL:
$mailer_instance = new $this->mailer['class']( $mailer_instance = new $this->mailer_config['class'](
$this->mailer['api_key'], $this->mailer_config['api_key'],
$this->sender, $this->sender,
$this->reply_to $this->reply_to
); );
break; break;
case 'MailGun': case self::METHOD_MAILGUN:
$mailer_instance = new $this->mailer['class']( $mailer_instance = new $this->mailer_config['class'](
$this->mailer['domain'], $this->mailer_config['domain'],
$this->mailer['api_key'], $this->mailer_config['api_key'],
$this->sender, $this->sender,
$this->reply_to $this->reply_to
); );
break; break;
case 'MailPoet': case self::METHOD_MAILPOET:
$mailer_instance = new $this->mailer['class']( $mailer_instance = new $this->mailer_config['class'](
$this->mailer['mailpoet_api_key'], $this->mailer_config['mailpoet_api_key'],
$this->sender, $this->sender,
$this->reply_to $this->reply_to
); );
break; break;
case 'SendGrid': case self::METHOD_SENDGRID:
$mailer_instance = new $this->mailer['class']( $mailer_instance = new $this->mailer_config['class'](
$this->mailer['api_key'], $this->mailer_config['api_key'],
$this->sender, $this->sender,
$this->reply_to $this->reply_to
); );
break; break;
case 'PHPMail': case self::METHOD_PHPMAIL:
$mailer_instance = new $this->mailer['class']( $mailer_instance = new $this->mailer_config['class'](
$this->sender, $this->sender,
$this->reply_to $this->reply_to
); );
break; break;
case 'SMTP': case self::METHOD_SMTP:
$mailer_instance = new $this->mailer['class']( $mailer_instance = new $this->mailer_config['class'](
$this->mailer['host'], $this->mailer_config['host'],
$this->mailer['port'], $this->mailer_config['port'],
$this->mailer['authentication'], $this->mailer_config['authentication'],
$this->mailer['login'], $this->mailer_config['login'],
$this->mailer['password'], $this->mailer_config['password'],
$this->mailer['encryption'], $this->mailer_config['encryption'],
$this->sender, $this->sender,
$this->reply_to $this->reply_to
); );
@ -89,12 +98,20 @@ class Mailer {
return $mailer_instance; return $mailer_instance;
} }
function getMailer($mailer = false) { static function getMailerConfig($mailer = false) {
if(!$mailer) { if(!$mailer) {
$mailer = Setting::getValue('mta'); $mailer = Setting::getValue(self::MAILER_CONFIG);
if(!$mailer || !isset($mailer['method'])) throw new \Exception(__('Mailer is not configured')); if(!$mailer || !isset($mailer['method'])) throw new \Exception(__('Mailer is not configured'));
} }
if(empty($mailer['frequency'])) {
$default_settings = Setting::getDefaults();
$mailer['frequency'] = $default_settings['mta']['frequency'];
}
// add additional variables to the mailer object
$mailer['class'] = 'MailPoet\\Mailer\\Methods\\' . $mailer['method']; $mailer['class'] = 'MailPoet\\Mailer\\Methods\\' . $mailer['method'];
$mailer['frequency_interval'] =
(int)$mailer['frequency']['interval'] * self::SENDING_LIMIT_INTERVAL_MULTIPLIER;
$mailer['frequency_limit'] = (int)$mailer['frequency']['emails'];
return $mailer; return $mailer;
} }

60
lib/Mailer/MailerLog.php Normal file
View File

@ -0,0 +1,60 @@
<?php
namespace MailPoet\Mailer;
use MailPoet\Models\Setting;
if(!defined('ABSPATH')) exit;
class MailerLog {
const SETTING_NAME = 'mta_log';
static function getMailerLog() {
$mailer_log = Setting::getValue(self::SETTING_NAME);
if(!$mailer_log) {
$mailer_log = self::createMailerLog();
}
return $mailer_log;
}
static function createMailerLog() {
$mailer_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue(self::SETTING_NAME, $mailer_log);
return $mailer_log;
}
static function resetMailerLog() {
return self::createMailerLog();
}
static function updateMailerLog($mailer_log) {
Setting::setValue(self::SETTING_NAME, $mailer_log);
return $mailer_log;
}
static function incrementSentCount($mailer_log = false) {
$mailer_log = ($mailer_log) ? $mailer_log : self::getMailerLog();
(int)$mailer_log['sent']++;
return self::updateMailerLog($mailer_log);
}
static function isSendingLimitReached() {
$mailer_config = Mailer::getMailerConfig();
$mailer_log = self::getMailerLog();
$elapsed_time = time() - (int)$mailer_log['started'];
if($mailer_log['sent'] === $mailer_config['frequency_limit']) {
if($elapsed_time <= $mailer_config['frequency_interval']) return true;
// reset mailer log if enough time has passed since the limit was reached
self::resetMailerLog();
}
return false;
}
static function enforceSendingLimit() {
if(self::isSendingLimitReached()) {
throw new \Exception(__('Sending frequency limit has been reached'));
}
}
}

View File

@ -1,6 +1,8 @@
<?php <?php
namespace MailPoet\Models; namespace MailPoet\Models;
use MailPoet\Cron\CronTrigger;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class Setting extends Model { class Setting extends Model {
@ -38,8 +40,8 @@ class Setting extends Model {
'interval' => self::DEFAULT_SENDING_FREQUENCY_INTERVAL 'interval' => self::DEFAULT_SENDING_FREQUENCY_INTERVAL
) )
), ),
'task_scheduler' => array( CronTrigger::SETTING_NAME => array(
'method' => 'WordPress' 'method' => CronTrigger::DEFAULT_METHOD
), ),
'signup_confirmation' => array( 'signup_confirmation' => array(
'enabled' => true, 'enabled' => true,
@ -160,4 +162,9 @@ class Setting extends Model {
return $setting->save(); return $setting->save();
} }
public static function deleteValue($value) {
$value = self::where('name', $value)->findOne();
return ($value) ? $value->delete() : false;
}
} }

View File

@ -130,7 +130,7 @@ class Links {
static function save(array $links, $newsletter_id, $queue_id) { static function save(array $links, $newsletter_id, $queue_id) {
foreach($links as $link) { foreach($links as $link) {
if(empty($link['hash'] || empty($link['url']))) continue; if(empty($link['hash']) || empty($link['url'])) continue;
$newsletter_link = NewsletterLink::create(); $newsletter_link = NewsletterLink::create();
$newsletter_link->newsletter_id = $newsletter_id; $newsletter_link->newsletter_id = $newsletter_id;
$newsletter_link->queue_id = $queue_id; $newsletter_link->queue_id = $queue_id;

View File

@ -66,7 +66,7 @@ class MailChimp {
$connection = @fopen($url, 'r'); $connection = @fopen($url, 'r');
if(!$connection) { if(!$connection) {
return $this->processError('connection'); return $this->processError('connection');
} else { }
$i = 0; $i = 0;
$header = array(); $header = array();
while(!feof($connection)) { while(!feof($connection)) {
@ -90,20 +90,16 @@ class MailChimp {
} }
$i++; $i++;
} }
$bytes_fetched += strlen($buffer); $bytes_fetched += strlen($buffer);
if($bytes_fetched > $this->max_post_size) { if($bytes_fetched > $this->max_post_size) {
return $this->processError('size'); return $this->processError('size');
} }
} }
fclose($connection); fclose($connection);
} }
}
if(!count($subscribers)) { if(!count($subscribers)) {
return $this->processError('subscribers'); return $this->processError('subscribers');
} }
return array( return array(

View File

@ -6,11 +6,10 @@
<% block translations %> <% block translations %>
<%= localize({ <%= localize({
'daemonNotRunning': __('Daemon is not running'),
'daemonControlError': __('Cron daemon error'), 'daemonControlError': __('Cron daemon error'),
'loadingDaemonStatus': __('Loading daemon status...'), 'loadingDaemonStatus': __('Loading daemon status...'),
'cronDaemonIsRunning': __('Cron daemon is running.'), 'cronDaemonIsRunning': __('Cron daemon is running'),
'stop': __('Stop'),
'start': __('Start'),
'cronDaemonState': __('Cron is %$1s') 'cronDaemonState': __('Cron is %$1s')
}) %> }) %>
<% endblock %> <% endblock %>

View File

@ -39,10 +39,9 @@
<label> <label>
<input <input
type="radio" type="radio"
name="task_scheduler[method]" name="cron_trigger[method]"
value="WordPress" value="<%= cron_trigger.wordpress %>"
<% if not(settings.task_scheduler.method) <% if (settings.cron_trigger.method == cron_trigger.wordpress) %>
or (settings.task_scheduler.method == 'WordPress') %>
checked="checked" checked="checked"
<% endif %> <% endif %>
/><%= __('Visitors to your website (recommended)') %> /><%= __('Visitors to your website (recommended)') %>
@ -52,9 +51,9 @@
<label> <label>
<input <input
type="radio" type="radio"
name="task_scheduler[method]" name="cron_trigger[method]"
value="MailPoet" value="<%= cron_trigger.mailpoet %>"
<% if (settings.task_scheduler.method == 'MailPoet') %> <% if (settings.cron_trigger.method == cron_trigger.mailpoet) %>
checked="checked" checked="checked"
<% endif %> <% endif %>
/><%= __("MailPoet's own script. Doesn't work with [link]these hosts[/link].") /><%= __("MailPoet's own script. Doesn't work with [link]these hosts[/link].")