diff --git a/lib/Config/Initializer.php b/lib/Config/Initializer.php index 3c8720ccc8..a06d005eb2 100644 --- a/lib/Config/Initializer.php +++ b/lib/Config/Initializer.php @@ -68,35 +68,37 @@ class Initializer { 'SET TIME_ZONE = "' . Env::$db_timezone_offset. '"' )); - $subscribers = Env::$db_prefix . 'subscribers'; $settings = Env::$db_prefix . 'settings'; - $newsletters = Env::$db_prefix . 'newsletters'; - $newsletter_templates = Env::$db_prefix . 'newsletter_templates'; $segments = Env::$db_prefix . 'segments'; $forms = Env::$db_prefix . 'forms'; - $subscriber_segment = Env::$db_prefix . 'subscriber_segment'; - $newsletter_segment = Env::$db_prefix . 'newsletter_segment'; $custom_fields = Env::$db_prefix . 'custom_fields'; + $subscribers = Env::$db_prefix . 'subscribers'; + $subscriber_segment = Env::$db_prefix . 'subscriber_segment'; $subscriber_custom_field = Env::$db_prefix . 'subscriber_custom_field'; + $newsletter_segment = Env::$db_prefix . 'newsletter_segment'; + $sending_queues = Env::$db_prefix . 'sending_queues'; + $newsletters = Env::$db_prefix . 'newsletters'; + $newsletter_templates = Env::$db_prefix . 'newsletter_templates'; $newsletter_option_fields = Env::$db_prefix . 'newsletter_option_fields'; $newsletter_option = Env::$db_prefix . 'newsletter_option'; - $sending_queues = Env::$db_prefix . 'sending_queues'; $newsletter_statistics = Env::$db_prefix . 'newsletter_statistics'; + $newsletter_links = Env::$db_prefix . 'newsletter_links'; - define('MP_SUBSCRIBERS_TABLE', $subscribers); define('MP_SETTINGS_TABLE', $settings); - define('MP_NEWSLETTERS_TABLE', $newsletters); define('MP_SEGMENTS_TABLE', $segments); define('MP_FORMS_TABLE', $forms); + define('MP_CUSTOM_FIELDS_TABLE', $custom_fields); + define('MP_SUBSCRIBERS_TABLE', $subscribers); define('MP_SUBSCRIBER_SEGMENT_TABLE', $subscriber_segment); + define('MP_SUBSCRIBER_CUSTOM_FIELD_TABLE', $subscriber_custom_field); + define('MP_SENDING_QUEUES_TABLE', $sending_queues); + define('MP_NEWSLETTERS_TABLE', $newsletters); define('MP_NEWSLETTER_TEMPLATES_TABLE', $newsletter_templates); define('MP_NEWSLETTER_SEGMENT_TABLE', $newsletter_segment); - define('MP_CUSTOM_FIELDS_TABLE', $custom_fields); - define('MP_SUBSCRIBER_CUSTOM_FIELD_TABLE', $subscriber_custom_field); define('MP_NEWSLETTER_OPTION_FIELDS_TABLE', $newsletter_option_fields); - define('MP_NEWSLETTER_OPTION_TABLE', $newsletter_option); - define('MP_SENDING_QUEUES_TABLE', $sending_queues); define('MP_NEWSLETTER_STATISTICS_TABLE', $newsletter_statistics); + define('MP_NEWSLETTER_LINKS_TABLE', $newsletter_links); + define('MP_NEWSLETTER_OPTION_TABLE', $newsletter_option); } function runMigrator() { diff --git a/lib/Config/Migrator.php b/lib/Config/Migrator.php index e0a8242744..bb336b2389 100644 --- a/lib/Config/Migrator.php +++ b/lib/Config/Migrator.php @@ -10,19 +10,20 @@ class Migrator { $this->prefix = Env::$db_prefix; $this->charset = Env::$db_charset; $this->models = array( - 'subscribers', + 'segments', 'settings', + 'custom_fields', + 'sending_queues', + 'subscribers', + 'subscriber_segment', + 'subscriber_custom_field', 'newsletters', 'newsletter_templates', - 'segments', - 'subscriber_segment', - 'newsletter_segment', - 'custom_fields', - 'subscriber_custom_field', 'newsletter_option_fields', 'newsletter_option', - 'sending_queues', + 'newsletter_segment', 'newsletter_statistics', + 'newsletter_links', 'forms' ); } @@ -49,6 +50,72 @@ class Migrator { array_map($drop_table, $this->models); } + function segments() { + $attributes = array( + 'id mediumint(9) NOT NULL AUTO_INCREMENT,', + 'name varchar(90) NOT NULL,', + 'type varchar(90) NOT NULL DEFAULT "default",', + 'description varchar(250) NOT NULL,', + 'created_at TIMESTAMP NOT NULL DEFAULT 0,', + 'deleted_at TIMESTAMP NULL DEFAULT NULL,', + 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', + 'PRIMARY KEY (id),', + 'UNIQUE KEY name (name)' + ); + return $this->sqlify(__FUNCTION__, $attributes); + } + + function settings() { + $attributes = array( + 'id mediumint(9) NOT NULL AUTO_INCREMENT,', + 'name varchar(20) NOT NULL,', + 'value longtext,', + 'created_at TIMESTAMP NOT NULL DEFAULT 0,', + 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', + 'PRIMARY KEY (id),', + 'UNIQUE KEY name (name)' + ); + return $this->sqlify(__FUNCTION__, $attributes); + } + + function custom_fields() { + $attributes = array( + 'id mediumint(9) NOT NULL AUTO_INCREMENT,', + 'name varchar(90) NOT NULL,', + 'type varchar(90) NOT NULL,', + 'params longtext NOT NULL,', + 'created_at TIMESTAMP NOT NULL DEFAULT 0,', + 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', + 'PRIMARY KEY (id),', + 'UNIQUE KEY name (name)' + ); + return $this->sqlify(__FUNCTION__, $attributes); + } + + function sending_queues() { + $attributes = array( + 'id mediumint(9) NOT NULL AUTO_INCREMENT,', + 'newsletter_id mediumint(9) NOT NULL,', + 'newsletter_rendered_body longtext,', + 'newsletter_rendered_body_hash varchar(250) NULL DEFAULT NULL,', + 'newsletter_rendered_subject varchar(250) NULL DEFAULT NULL,', + 'subscribers longtext,', + 'status varchar(12) NULL DEFAULT NULL,', + 'priority mediumint(9) NOT NULL DEFAULT 0,', + 'count_total mediumint(9) NOT NULL DEFAULT 0,', + 'count_processed mediumint(9) NOT NULL DEFAULT 0,', + 'count_to_process mediumint(9) NOT NULL DEFAULT 0,', + 'count_failed mediumint(9) NOT NULL DEFAULT 0,', + 'scheduled_at TIMESTAMP NOT NULL DEFAULT 0,', + 'processed_at TIMESTAMP NOT NULL DEFAULT 0,', + 'created_at TIMESTAMP NOT NULL DEFAULT 0,', + 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', + 'deleted_at TIMESTAMP NULL DEFAULT NULL,', + 'PRIMARY KEY (id)', + ); + return $this->sqlify(__FUNCTION__, $attributes); + } + function subscribers() { $attributes = array( 'id mediumint(9) NOT NULL AUTO_INCREMENT,', @@ -66,15 +133,30 @@ class Migrator { return $this->sqlify(__FUNCTION__, $attributes); } - function settings() { + function subscriber_segment() { $attributes = array( 'id mediumint(9) NOT NULL AUTO_INCREMENT,', - 'name varchar(20) NOT NULL,', - 'value longtext,', + 'subscriber_id mediumint(9) NOT NULL,', + 'segment_id mediumint(9) NOT NULL,', + 'status varchar(12) NOT NULL DEFAULT "subscribed",', 'created_at TIMESTAMP NOT NULL DEFAULT 0,', 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', 'PRIMARY KEY (id),', - 'UNIQUE KEY name (name)' + 'UNIQUE KEY subscriber_segment (subscriber_id,segment_id)' + ); + return $this->sqlify(__FUNCTION__, $attributes); + } + + function subscriber_custom_field() { + $attributes = array( + 'id mediumint(9) NOT NULL AUTO_INCREMENT,', + 'subscriber_id mediumint(9) NOT NULL,', + 'custom_field_id mediumint(9) NOT NULL,', + 'value varchar(255) NOT NULL,', + 'created_at TIMESTAMP NOT NULL DEFAULT 0,', + 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', + 'PRIMARY KEY (id),', + 'UNIQUE KEY subscriber_id_custom_field_id (subscriber_id,custom_field_id)' ); return $this->sqlify(__FUNCTION__, $attributes); } @@ -113,76 +195,6 @@ class Migrator { return $this->sqlify(__FUNCTION__, $attributes); } - function segments() { - $attributes = array( - 'id mediumint(9) NOT NULL AUTO_INCREMENT,', - 'name varchar(90) NOT NULL,', - 'type varchar(90) NOT NULL DEFAULT "default",', - 'description varchar(250) NOT NULL,', - 'created_at TIMESTAMP NOT NULL DEFAULT 0,', - 'deleted_at TIMESTAMP NULL DEFAULT NULL,', - 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', - 'PRIMARY KEY (id),', - 'UNIQUE KEY name (name)' - ); - return $this->sqlify(__FUNCTION__, $attributes); - } - - function subscriber_segment() { - $attributes = array( - 'id mediumint(9) NOT NULL AUTO_INCREMENT,', - 'subscriber_id mediumint(9) NOT NULL,', - 'segment_id mediumint(9) NOT NULL,', - 'status varchar(12) NOT NULL DEFAULT "subscribed",', - 'created_at TIMESTAMP NOT NULL DEFAULT 0,', - 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', - 'PRIMARY KEY (id),', - 'UNIQUE KEY subscriber_segment (subscriber_id,segment_id)' - ); - return $this->sqlify(__FUNCTION__, $attributes); - } - - function newsletter_segment() { - $attributes = array( - 'id mediumint(9) NOT NULL AUTO_INCREMENT,', - 'newsletter_id mediumint(9) NOT NULL,', - 'segment_id mediumint(9) NOT NULL,', - 'created_at TIMESTAMP NOT NULL DEFAULT 0,', - 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', - 'PRIMARY KEY (id),', - 'UNIQUE KEY newsletter_segment (newsletter_id,segment_id)' - ); - return $this->sqlify(__FUNCTION__, $attributes); - } - - function custom_fields() { - $attributes = array( - 'id mediumint(9) NOT NULL AUTO_INCREMENT,', - 'name varchar(90) NOT NULL,', - 'type varchar(90) NOT NULL,', - 'params longtext NOT NULL,', - 'created_at TIMESTAMP NOT NULL DEFAULT 0,', - 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', - 'PRIMARY KEY (id),', - 'UNIQUE KEY name (name)' - ); - return $this->sqlify(__FUNCTION__, $attributes); - } - - function subscriber_custom_field() { - $attributes = array( - 'id mediumint(9) NOT NULL AUTO_INCREMENT,', - 'subscriber_id mediumint(9) NOT NULL,', - 'custom_field_id mediumint(9) NOT NULL,', - 'value varchar(255) NOT NULL,', - 'created_at TIMESTAMP NOT NULL DEFAULT 0,', - 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', - 'PRIMARY KEY (id),', - 'UNIQUE KEY subscriber_id_custom_field_id (subscriber_id,custom_field_id)' - ); - return $this->sqlify(__FUNCTION__, $attributes); - } - function newsletter_option_fields() { $attributes = array( 'id mediumint(9) NOT NULL AUTO_INCREMENT,', @@ -210,26 +222,15 @@ class Migrator { return $this->sqlify(__FUNCTION__, $attributes); } - function sending_queues() { + function newsletter_segment() { $attributes = array( 'id mediumint(9) NOT NULL AUTO_INCREMENT,', 'newsletter_id mediumint(9) NOT NULL,', - 'newsletter_rendered_body longtext,', - 'newsletter_rendered_body_hash varchar(250) NULL DEFAULT NULL,', - 'newsletter_rendered_subject varchar(250) NULL DEFAULT NULL,', - 'subscribers longtext,', - 'status varchar(12) NULL DEFAULT NULL,', - 'priority mediumint(9) NOT NULL DEFAULT 0,', - 'count_total mediumint(9) NOT NULL DEFAULT 0,', - 'count_processed mediumint(9) NOT NULL DEFAULT 0,', - 'count_to_process mediumint(9) NOT NULL DEFAULT 0,', - 'count_failed mediumint(9) NOT NULL DEFAULT 0,', - 'scheduled_at TIMESTAMP NOT NULL DEFAULT 0,', - 'processed_at TIMESTAMP NOT NULL DEFAULT 0,', + 'segment_id mediumint(9) NOT NULL,', 'created_at TIMESTAMP NOT NULL DEFAULT 0,', 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', - 'deleted_at TIMESTAMP NULL DEFAULT NULL,', - 'PRIMARY KEY (id)', + 'PRIMARY KEY (id),', + 'UNIQUE KEY newsletter_segment (newsletter_id,segment_id)' ); return $this->sqlify(__FUNCTION__, $attributes); } @@ -246,6 +247,20 @@ class Migrator { return $this->sqlify(__FUNCTION__, $attributes); } + function newsletter_links() { + $attributes = array( + 'id mediumint(9) NOT NULL AUTO_INCREMENT,', + 'newsletter_id mediumint(9) NOT NULL,', + 'queue_id mediumint(9) NOT NULL,', + 'url varchar(255) NOT NULL,', + 'hash varchar(20) NOT NULL,', + 'created_at TIMESTAMP NOT NULL DEFAULT 0,', + 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', + 'PRIMARY KEY (id)', + ); + return $this->sqlify(__FUNCTION__, $attributes); + } + function forms() { $attributes = array( 'id mediumint(9) NOT NULL AUTO_INCREMENT,', diff --git a/lib/Cron/Workers/SendingQueue.php b/lib/Cron/Workers/SendingQueue.php index 245bc53f40..78ddd48be4 100644 --- a/lib/Cron/Workers/SendingQueue.php +++ b/lib/Cron/Workers/SendingQueue.php @@ -4,9 +4,11 @@ namespace MailPoet\Cron\Workers; use MailPoet\Cron\CronHelper; use MailPoet\Mailer\Mailer; use MailPoet\Models\Newsletter; +use MailPoet\Models\NewsletterLink; use MailPoet\Models\NewsletterStatistics; use MailPoet\Models\Setting; use MailPoet\Models\Subscriber; +use MailPoet\Newsletter\Links\Links; use MailPoet\Newsletter\Renderer\Renderer; use MailPoet\Newsletter\Shortcodes\Shortcodes; use MailPoet\Util\Helpers; @@ -17,6 +19,7 @@ class SendingQueue { public $mta_config; public $mta_log; public $processing_method; + public $divider = '***MailPoet***'; private $timer; const batch_size = 50; @@ -38,7 +41,7 @@ class SendingQueue { continue; } $newsletter = $newsletter->asArray(); - $newsletter['body'] = $this->getNewsletterBodyAndSubject($queue, $newsletter); + $newsletter['body'] = $this->getOrRenderNewsletterBody($queue, $newsletter); $queue->subscribers = (object) unserialize($queue->subscribers); if(!isset($queue->subscribers->processed)) { $queue->subscribers->processed = array(); @@ -67,11 +70,20 @@ class SendingQueue { } } - function getNewsletterBodyAndSubject($queue, $newsletter) { + function getOrRenderNewsletterBody($queue, $newsletter) { // check if newsletter has been rendered, in which case return its contents - // or render & and for future use + // or render and save for future reuse if($queue->newsletter_rendered_body === null) { - $newsletter['body'] = $this->renderNewsletter($newsletter); + // render newsletter + $rendered_newsletter = $this->renderNewsletter($newsletter); + // extract and replace links + $processed_newsletter = $this->processLinks( + $this->joinObject($rendered_newsletter), + $newsletter['id'], + $queue->id + ); + list($newsletter['body']['html'], $newsletter['body']['text']) = + $this->splitObject($processed_newsletter); $queue->newsletter_rendered_body = json_encode($newsletter['body']); $queue->newsletter_rendered_body_hash = md5($newsletter['body']['text']); $queue->save(); @@ -84,7 +96,7 @@ class SendingQueue { function processBulkSubscribers($mailer, $newsletter, $subscribers, $queue) { foreach($subscribers as $subscriber) { $processed_newsletters[] = - $this->processNewsletter($newsletter, $subscriber); + $this->processNewsletter($newsletter, $subscriber, $queue); $transformed_subscribers[] = $mailer->transformSubscriber($subscriber); } @@ -125,8 +137,8 @@ class SendingQueue { function processIndividualSubscriber($mailer, $newsletter, $subscribers, $queue) { foreach($subscribers as $subscriber) { $this->checkSendingLimit(); - $processed_newsletter = $this->processNewsletter($newsletter, $subscriber); - if (!$queue->newsletter_rendered_subject) { + $processed_newsletter = $this->processNewsletter($newsletter, $subscriber, $queue); + if(!$queue->newsletter_rendered_subject) { $queue->newsletter_rendered_subject = $processed_newsletter['subject']; } $transformed_subscriber = $mailer->transformSubscriber($subscriber); @@ -162,22 +174,61 @@ class SendingQueue { return $renderer->render(); } - function processNewsletter($newsletter, $subscriber = false) { - $divider = '***MailPoet***'; - $data_for_shortcodes = - array_merge(array($newsletter['subject']), $newsletter['body']); - $body = implode($divider, $data_for_shortcodes); - $shortcodes = new Shortcodes( + function processLinks($text, $newsletter_id, $queue_id) { + $links = new Links(); + list($text, $processed_links) = $links->replace($text); + foreach($processed_links as $link) { + // save extracted and processed links + $newsletter_link = NewsletterLink::create(); + $newsletter_link->newsletter_id = $newsletter_id; + $newsletter_link->queue_id = $queue_id; + $newsletter_link->hash = $link['hash']; + $newsletter_link->url = $link['url']; + $newsletter_link->save(); + } + return $text; + } + + function processNewsletter($newsletter, $subscriber = false, $queue) { + $data_for_shortcodes = array( + $newsletter['subject'], + $newsletter['body']['html'], + $newsletter['body']['text'] + ); + $processed_newsletter = $this->replaceShortcodes( $newsletter, - $subscriber + $subscriber, + $this->joinObject($data_for_shortcodes) + ); + $processed_newsletter = $this->replaceLinks( + $newsletter['id'], + $subscriber['id'], + $queue->id, + $processed_newsletter ); list($newsletter['subject'], $newsletter['body']['html'], $newsletter['body']['text'] - ) = explode($divider, $shortcodes->replace($body)); + ) = $this->splitObject($processed_newsletter); return $newsletter; } + function replaceLinks($newsletter_id, $subscriber_id, $queue_id, $body) { + return str_replace( + '[mailpoet_data]', + sprintf('%s-%s-%s', $newsletter_id, $subscriber_id, $queue_id), + $body + ); + } + + function replaceShortcodes($newsletter, $subscriber, $body) { + $shortcodes = new Shortcodes( + $newsletter, + $subscriber + ); + return $shortcodes->replace($body); + } + function sendNewsletter($mailer, $newsletter, $subscriber) { return $mailer->mailer_instance->send( $newsletter, @@ -284,4 +335,12 @@ class SendingQueue { } return; } + + private function joinObject($object = array()) { + return implode($this->divider, $object); + } + + private function splitObject($object = array()) { + return explode($this->divider, $object); + } } \ No newline at end of file diff --git a/lib/Models/NewsletterLink.php b/lib/Models/NewsletterLink.php new file mode 100644 index 0000000000..ea60d8c10d --- /dev/null +++ b/lib/Models/NewsletterLink.php @@ -0,0 +1,12 @@ +newsletter_id = $newsletter_id; + $this->queue_id = $queue_id; + $this->subscriber_id = $subscriber_id; + } + + function extract($text) { + // adopted from WP's wp_extract_urls() function & modified to work on hrefs + $regex = '#(?:href.*?=.*?)(["\']?)(' + . '(?:([\w-]+:)?//?)' + . '[^\s()<>]+' + . '[.]' + . '(?:' + . '\([\w\d]+\)|' + . '(?:' + . '[^`!()\[\]{};:\'".,<>«»“”‘’\s]|' + . '(?:[:]\d+)?/?' + . ')+' + . ')' + . ')\\1#'; + preg_match_all($regex, $text, $links); + preg_match_all('/\[\w+:\w+\]/', $text, $shortcodes); + return array_merge( + array_unique($links[2]), + array_unique($shortcodes[0]) + ); + } + + function replace($text, $links = false) { + $links = ($links) ? $links : $this->extract($text); + $processed_links = array(); + foreach($links as $link) { + $hash = Security::generateRandomString(5); + $processed_links[] = array( + 'hash' => $hash, + 'url' => $link + ); + $encoded_link = sprintf( + '%s/?mailpoet&endpoint=track&data=%s', + home_url(), + '[mailpoet_data]-'.$hash + ); + $link_regex = '/' . preg_quote($link, '/') . '/'; + $text = preg_replace($link_regex, $encoded_link, $text); + } + return array($text, $processed_links); + } +} \ No newline at end of file diff --git a/lib/Newsletter/Shortcodes/Shortcodes.php b/lib/Newsletter/Shortcodes/Shortcodes.php index 22c39f39ef..67ad4f2177 100644 --- a/lib/Newsletter/Shortcodes/Shortcodes.php +++ b/lib/Newsletter/Shortcodes/Shortcodes.php @@ -26,9 +26,12 @@ class Shortcodes { $shortcode, $shortcode_details ); - $shortcode_class = - __NAMESPACE__ . '\\Categories\\' . ucfirst($shortcode_details['type']); + $shortcode_type = ucfirst($shortcode_details['type']); $shortcode_action = $shortcode_details['action']; + // do not process subscription management links + if ($shortcode_type === 'Subscription') return $shortcode; + $shortcode_class = + __NAMESPACE__ . '\\Categories\\' . $shortcode_type; $shortcode_default_value = isset($shortcode_details['default']) ? $shortcode_details['default'] : false; if(!class_exists($shortcode_class)) return false;