Files
piratepoet/mailpoet/lib/Config/Migrator.php
John Oleksowicz c2612f4511 Shorten the max tag name from 255 to 191 chars
MySQL has maximum key lengths for indexes, and the number can vary based
 on storage engine and MySQL version. Using utf8mb4, the index would be
 255 * 4 = 1020 bytes long, exceeding the MyISAM maximum of 1000 bytes
 and the MySQL 5.6 maximum of 767 bytes.

 See Column Prefix Key Parts in the MySQL reference:
 https://dev.mysql.com/doc/refman/8.0/en/create-index.html

 By reducing this length to 191, we ensure that the maximum likely index
 length will be less than the 767 lower bound (191 * 4 = 764).

 MAILPOET-4489
2022-07-20 09:30:43 +02:00

1020 lines
38 KiB
PHP

<?php
namespace MailPoet\Config;
use MailPoet\Cron\CronTrigger;
use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Entities\FormEntity;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
use MailPoet\Segments\DynamicSegments\Filters\UserRole;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription;
use MailPoet\Settings\SettingsChangeHandler;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Helpers;
// The "created_at" column must be NULL in some tables to avoid "there can be only one
// TIMESTAMP column with CURRENT_TIMESTAMP" error on MySQL version < 5.6.5 that occurs
// even when other timestamp is simply "NOT NULL".
class Migrator {
public $prefix;
private $charsetCollate;
private $models;
/** @var SettingsController */
private $settings;
/** @var SettingsChangeHandler */
private $settingsChangeHandler;
public function __construct(
SettingsController $settings,
SettingsChangeHandler $settingsChangeHandler
) {
$this->settings = $settings;
$this->settingsChangeHandler = $settingsChangeHandler;
$this->prefix = Env::$dbPrefix;
$this->charsetCollate = Env::$dbCharsetCollate;
$this->models = [
'segments',
'settings',
'custom_fields',
'scheduled_tasks',
'stats_notifications',
'scheduled_task_subscribers',
'sending_queues',
'subscribers',
'subscriber_segment',
'subscriber_custom_field',
'subscriber_ips',
'newsletters',
'newsletter_templates',
'newsletter_option_fields',
'newsletter_option',
'newsletter_segment',
'newsletter_links',
'newsletter_posts',
'forms',
'statistics_newsletters',
'statistics_clicks',
'statistics_bounces',
'statistics_opens',
'statistics_unsubscribes',
'statistics_forms',
'statistics_woocommerce_purchases',
'mapping_to_external_entities',
'log',
'user_flags',
'feature_flags',
'dynamic_segment_filters',
'user_agents',
'tags',
'subscriber_tag',
];
}
public function up() {
global $wpdb;
// Ensure dbDelta function
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
$output = [];
foreach ($this->models as $model) {
$modelMethod = Helpers::underscoreToCamelCase($model);
$output = array_merge(dbDelta($this->$modelMethod()), $output);
}
$this->updateNullInUnsubscribeStats();
$this->fixScheduledTasksSubscribersTimestampColumns();
$this->removeDeprecatedStatisticsIndexes();
$this->migrateSerializedFilterDataToNewColumns();
$this->migratePurchasedProductDynamicFilters();
$this->migrateWooSubscriptionsDynamicFilters();
$this->migratePurchasedInCategoryDynamicFilters();
$this->migrateEmailActionsFilters();
$this->updateDefaultInactiveSubscriberTimeRange();
$this->setDefaultValueForLoadingThirdPartyLibrariesForExistingInstalls();
$this->disableMailPoetCronTrigger();
return $output;
}
public function down() {
global $wpdb;
$_this = $this;
$dropTable = function($model) use($wpdb, $_this) {
$table = esc_sql($_this->prefix . $model);
$wpdb->query("DROP TABLE {$table}");
};
array_map($dropTable, $this->models);
}
public function segments() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'type varchar(90) NOT NULL DEFAULT "default",',
'description varchar(250) NOT NULL DEFAULT "",',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at timestamp NULL,',
'average_engagement_score FLOAT unsigned NULL,',
'average_engagement_score_updated_at timestamp NULL,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name),',
'KEY average_engagement_score_updated_at (average_engagement_score_updated_at)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function settings() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'name varchar(50) NOT NULL,',
'value longtext,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function customFields() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'type varchar(90) NOT NULL,',
'params longtext NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function scheduledTasks() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'type varchar(90) NULL DEFAULT NULL,',
'status varchar(12) NULL DEFAULT NULL,',
'priority mediumint(9) NOT NULL DEFAULT 0,',
'scheduled_at timestamp NULL,',
'processed_at timestamp NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at timestamp NULL,',
'in_progress int(1),',
'reschedule_count int(11) NOT NULL DEFAULT 0,',
'meta longtext,',
'PRIMARY KEY (id),',
'KEY type (type),',
'KEY status (status)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function statsNotifications() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'task_id int(11) unsigned NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY newsletter_id_task_id (newsletter_id, task_id),',
'KEY task_id (task_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function disableMailPoetCronTrigger() {
$method = $this->settings->get(CronTrigger::SETTING_NAME . '.method');
if ($method !== 'MailPoet') {
return;
}
$this->settings->set(CronTrigger::SETTING_NAME . '.method', CronTrigger::METHOD_WORDPRESS);
}
public function scheduledTaskSubscribers() {
$attributes = [
'task_id int(11) unsigned NOT NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'processed int(1) NOT NULL,',
'failed smallint(1) NOT NULL DEFAULT 0,',
'error text NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (task_id, subscriber_id),',
'KEY subscriber_id (subscriber_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function sendingQueues() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'task_id int(11) unsigned NOT NULL,',
'newsletter_id int(11) unsigned NOT NULL,',
'newsletter_rendered_body longtext,',
'newsletter_rendered_subject varchar(250) NULL DEFAULT NULL,',
'subscribers longtext,',
'count_total int(11) unsigned NOT NULL DEFAULT 0,',
'count_processed int(11) unsigned NOT NULL DEFAULT 0,',
'count_to_process int(11) unsigned NOT NULL DEFAULT 0,',
'meta longtext,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at timestamp NULL,',
'PRIMARY KEY (id),',
'KEY task_id (task_id),',
'KEY newsletter_id (newsletter_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function subscribers() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'wp_user_id bigint(20) NULL,',
'is_woocommerce_user int(1) NOT NULL DEFAULT 0,',
'first_name varchar(255) NOT NULL DEFAULT "",',
'last_name varchar(255) NOT NULL DEFAULT "",',
'email varchar(150) NOT NULL,',
'status varchar(12) NOT NULL DEFAULT "' . SubscriberEntity::STATUS_UNCONFIRMED . '",',
'subscribed_ip varchar(45) NULL,',
'confirmed_ip varchar(45) NULL,',
'confirmed_at timestamp NULL,',
'last_subscribed_at timestamp NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at timestamp NULL,',
'unconfirmed_data longtext,',
"source enum('form','imported','administrator','api','wordpress_user','woocommerce_user','woocommerce_checkout','unknown') DEFAULT 'unknown',",
'count_confirmations int(11) unsigned NOT NULL DEFAULT 0,',
'unsubscribe_token char(15) NULL,',
'link_token char(32) NULL,',
'engagement_score FLOAT unsigned NULL,',
'engagement_score_updated_at timestamp NULL,',
'last_engagement_at timestamp NULL,',
'woocommerce_synced_at timestamp NULL,',
'email_count int(11) unsigned NOT NULL DEFAULT 0, ',
'PRIMARY KEY (id),',
'UNIQUE KEY email (email),',
'UNIQUE KEY unsubscribe_token (unsubscribe_token),',
'KEY wp_user_id (wp_user_id),',
'KEY updated_at (updated_at),',
'KEY status_deleted_at (status,deleted_at),',
'KEY last_subscribed_at (last_subscribed_at),',
'KEY engagement_score_updated_at (engagement_score_updated_at),',
'KEY link_token (link_token)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function subscriberSegment() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'subscriber_id int(11) unsigned NOT NULL,',
'segment_id int(11) unsigned NOT NULL,',
'status varchar(12) NOT NULL DEFAULT "' . SubscriberEntity::STATUS_SUBSCRIBED . '",',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY subscriber_segment (subscriber_id,segment_id),',
'KEY segment_id (segment_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function subscriberCustomField() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'subscriber_id int(11) unsigned NOT NULL,',
'custom_field_id int(11) unsigned NOT NULL,',
'value text NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'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);
}
public function subscriberIps() {
$attributes = [
'ip varchar(45) NOT NULL,',
'created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (created_at, ip),',
'KEY ip (ip)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function newsletters() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'hash varchar(150) NULL DEFAULT NULL,',
'parent_id int(11) unsigned NULL,',
'subject varchar(250) NOT NULL DEFAULT "",',
'type varchar(20) NOT NULL DEFAULT "standard",',
'sender_address varchar(150) NOT NULL DEFAULT "",',
'sender_name varchar(150) NOT NULL DEFAULT "",',
'status varchar(20) NOT NULL DEFAULT "' . NewsletterEntity::STATUS_DRAFT . '",',
'reply_to_address varchar(150) NOT NULL DEFAULT "",',
'reply_to_name varchar(150) NOT NULL DEFAULT "",',
'preheader varchar(250) NOT NULL DEFAULT "",',
'body longtext,',
'sent_at timestamp NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at timestamp NULL,',
'unsubscribe_token char(15) NULL,',
'ga_campaign varchar(250) NOT NULL DEFAULT "",',
'PRIMARY KEY (id),',
'UNIQUE KEY unsubscribe_token (unsubscribe_token),',
'KEY type_status (type,status)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function newsletterTemplates() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) NULL DEFAULT 0,',
'name varchar(250) NOT NULL,',
'categories varchar(250) NOT NULL DEFAULT "[]",',
'description varchar(255) NOT NULL DEFAULT "",',
'body longtext,',
'thumbnail longtext,',
'thumbnail_data longtext,',
'readonly tinyint(1) DEFAULT 0,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function newsletterOptionFields() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'newsletter_type varchar(90) NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name_newsletter_type (newsletter_type,name)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function newsletterOption() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'option_field_id int(11) unsigned NOT NULL,',
'value longtext,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY newsletter_id_option_field_id (newsletter_id,option_field_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function newsletterSegment() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'segment_id int(11) unsigned NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'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);
}
public function newsletterLinks() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'queue_id int(11) unsigned NOT NULL,',
'url varchar(2083) NOT NULL,',
'hash varchar(20) NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY queue_id (queue_id),',
'KEY url (url(100))',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function newsletterPosts() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'post_id int(11) unsigned NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function forms() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,', // should be null but db_delta can't handle this change
'status varchar(20) NOT NULL DEFAULT "' . FormEntity::STATUS_ENABLED . '",',
'body longtext,',
'settings longtext,',
'styles longtext,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at timestamp NULL,',
'PRIMARY KEY (id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function statisticsNewsletters() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'queue_id int(11) unsigned NOT NULL,',
'sent_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY subscriber_id (subscriber_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function statisticsBounces() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'queue_id int(11) unsigned NOT NULL,',
'created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function statisticsClicks() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'queue_id int(11) unsigned NOT NULL,',
'link_id int(11) unsigned NOT NULL,',
'user_agent_id int(11) unsigned NULL,',
'user_agent_type tinyint(1) NOT NULL DEFAULT 0,',
'count int(11) unsigned NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id_subscriber_id_user_agent_type (newsletter_id, subscriber_id, user_agent_type),',
'KEY queue_id (queue_id),',
'KEY subscriber_id (subscriber_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function statisticsOpens() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'queue_id int(11) unsigned NOT NULL,',
'user_agent_id int(11) unsigned NULL,',
'user_agent_type tinyint(1) NOT NULL DEFAULT 0,',
'created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id_subscriber_id_user_agent_type (newsletter_id, subscriber_id, user_agent_type),',
'KEY queue_id (queue_id),',
'KEY subscriber_id (subscriber_id),',
'KEY created_at (created_at),',
'KEY subscriber_id_created_at (subscriber_id, created_at)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function statisticsUnsubscribes() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'queue_id int(11) unsigned NULL,',
'created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,',
"source varchar(255) DEFAULT 'unknown',",
'meta varchar(255) NULL,',
'PRIMARY KEY (id),',
'KEY newsletter_id_subscriber_id (newsletter_id, subscriber_id),',
'KEY queue_id (queue_id),',
'KEY subscriber_id (subscriber_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function statisticsForms() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'form_id int(11) unsigned NOT NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY form_subscriber (form_id,subscriber_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function statisticsWoocommercePurchases() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'newsletter_id int(11) unsigned NOT NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'queue_id int(11) unsigned NOT NULL,',
'click_id int(11) unsigned NOT NULL,',
'order_id bigint(20) unsigned NOT NULL,',
'order_currency char(3) NOT NULL,',
'order_price_total float NOT NULL COMMENT "With shipping and taxes in order_currency",',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY queue_id (queue_id),',
'KEY subscriber_id (subscriber_id),',
'UNIQUE KEY click_id_order_id (click_id, order_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function mappingToExternalEntities() {
$attributes = [
'old_id int(11) unsigned NOT NULL,',
'type varchar(50) NOT NULL,',
'new_id int(11) unsigned NOT NULL,',
'created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (old_id, type),',
'KEY new_id (new_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function log() {
$attributes = [
'id bigint(20) unsigned NOT NULL AUTO_INCREMENT,',
'name varchar(255),',
'level int(11),',
'message longtext,',
'created_at timestamp DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function userFlags() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'user_id bigint(20) NOT NULL,',
'name varchar(50) NOT NULL,',
'value varchar(255),',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY user_id_name (user_id, name)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function featureFlags() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'name varchar(100) NOT NULL,',
'value tinyint(1),',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function dynamicSegmentFilters() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'segment_id int(11) unsigned NOT NULL,',
'created_at timestamp NULL,',
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'filter_data longblob,',
'filter_type varchar(255) NULL,',
'action varchar(255) NULL,',
'PRIMARY KEY (id),',
'KEY segment_id (segment_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function userAgents() {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'hash varchar(32) UNIQUE NOT NULL, ',
'user_agent text NOT NULL, ',
'created_at timestamp NULL,',
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function tags(): string {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'name varchar(191) NOT NULL,',
'description text NOT NULL DEFAULT "",',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
public function subscriberTag(): string {
$attributes = [
'id int(11) unsigned NOT NULL AUTO_INCREMENT,',
'subscriber_id int(11) unsigned NOT NULL,',
'tag_id int(11) unsigned NOT NULL,',
'created_at timestamp NULL,', // must be NULL, see comment at the top
'updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY subscriber_tag (subscriber_id, tag_id),',
'KEY tag_id (tag_id)',
];
return $this->sqlify(__FUNCTION__, $attributes);
}
private function sqlify($model, $attributes) {
$table = $this->prefix . Helpers::camelCaseToUnderscore($model);
$sql = [];
$sql[] = "CREATE TABLE " . $table . " (";
$sql = array_merge($sql, $attributes);
$sql[] = ") " . $this->charsetCollate . ";";
return implode("\n", $sql);
}
private function updateNullInUnsubscribeStats() {
global $wpdb;
// perform once for versions below or equal to 3.47.6
if (version_compare((string)$this->settings->get('db_version', '3.47.6'), '3.47.6', '>')) {
return false;
}
$table = esc_sql("{$this->prefix}statistics_unsubscribes");
$query = "
ALTER TABLE `{$table}`
CHANGE `newsletter_id` `newsletter_id` int(11) unsigned NULL,
CHANGE `queue_id` `queue_id` int(11) unsigned NULL;
";
$wpdb->query($query);
return true;
}
/**
* This method adds updated_at column to scheduled_task_subscribers for users with old MySQL..
* Updated_at was added after created_at column and created_at used to have default CURRENT_TIMESTAMP.
* Since MySQL versions below 5.6.5 allow only one column with CURRENT_TIMESTAMP as default per table
* and db_delta doesn't remove default values we need to perform this change manually..
* @return bool
*/
private function fixScheduledTasksSubscribersTimestampColumns() {
// skip the migration if the DB version is higher than 3.63.0 or is not set (a new install)
if (version_compare((string)$this->settings->get('db_version', '3.63.1'), '3.63.0', '>')) {
return false;
}
global $wpdb;
$scheduledTasksSubscribersTable = esc_sql("{$this->prefix}scheduled_task_subscribers");
// Remove default CURRENT_TIMESTAMP from created_at
$updateCreatedAtQuery = "
ALTER TABLE `$scheduledTasksSubscribersTable`
CHANGE `created_at` `created_at` timestamp NULL;
";
$wpdb->query($updateCreatedAtQuery);
// Add updated_at column in case it doesn't exist
$updatedAtColumnExists = $wpdb->get_results($wpdb->prepare("
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = %s AND column_name = 'updated_at';
", $scheduledTasksSubscribersTable));
if (empty($updatedAtColumnExists)) {
$addUpdatedAtQuery = "
ALTER TABLE `$scheduledTasksSubscribersTable`
ADD `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
";
$wpdb->query($addUpdatedAtQuery);
}
return true;
}
private function removeDeprecatedStatisticsIndexes(): bool {
global $wpdb;
// skip the migration if the DB version is higher than 3.67.1 or is not set (a new install)
if (version_compare((string)$this->settings->get('db_version', '3.67.1'), '3.67.1', '>')) {
return false;
}
$dbName = Env::$dbName;
$statisticsTables = [
esc_sql("{$this->prefix}statistics_clicks"),
esc_sql("{$this->prefix}statistics_opens"),
];
foreach ($statisticsTables as $statisticsTable) {
$oldStatisticsIndexExists = $wpdb->get_results($wpdb->prepare("
SELECT DISTINCT INDEX_NAME
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = %s
AND TABLE_NAME = %s
AND INDEX_NAME='newsletter_id_subscriber_id'
", $dbName, $statisticsTable));
if (!empty($oldStatisticsIndexExists)) {
$dropIndexQuery = "
ALTER TABLE `{$statisticsTable}`
DROP INDEX `newsletter_id_subscriber_id`
";
$wpdb->query($dropIndexQuery);
}
}
return true;
}
private function migrateSerializedFilterDataToNewColumns(): bool {
global $wpdb;
// skip the migration if the DB version is higher than 3.73.1 or is not set (a new install)
if (version_compare((string)$this->settings->get('db_version', '3.73.1'), '3.73.0', '>')) {
return false;
}
$dynamicSegmentFiltersTable = esc_sql("{$this->prefix}dynamic_segment_filters");
$dynamicSegmentFilters = $wpdb->get_results("
SELECT id, filter_data, filter_type, `action`
FROM {$dynamicSegmentFiltersTable}
", ARRAY_A);
foreach ($dynamicSegmentFilters as $dynamicSegmentFilter) {
if ($dynamicSegmentFilter['filter_type'] && $dynamicSegmentFilter['action']) {
continue;
}
$filterData = unserialize($dynamicSegmentFilter['filter_data']);
// bc compatibility fix, the filter with the segmentType userRole didn't have filled action
if ($filterData['segmentType'] === DynamicSegmentFilterData::TYPE_USER_ROLE && empty($filterData['action'])) {
$filterData['action'] = UserRole::TYPE;
}
$wpdb->update($dynamicSegmentFiltersTable, [
'action' => $filterData['action'] ?? null,
'filter_type' => $filterData['segmentType'] ?? null,
], ['id' => $dynamicSegmentFilter['id']]);
}
return true;
}
private function migratePurchasedProductDynamicFilters(): bool {
global $wpdb;
// skip the migration if the DB version is higher than 3.74.3 or is not set (a new install)
if (version_compare((string)$this->settings->get('db_version', '3.74.3'), '3.74.2', '>')) {
return false;
}
$dynamicSegmentFiltersTable = esc_sql("{$this->prefix}dynamic_segment_filters");
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE;
$action = WooCommerceProduct::ACTION_PRODUCT;
$dynamicSegmentFilters = $wpdb->get_results("
SELECT `id`, `filter_data`, `filter_type`, `action`
FROM {$dynamicSegmentFiltersTable}
WHERE `filter_type` = '{$filterType}'
AND `action` = '{$action}'
", ARRAY_A);
foreach ($dynamicSegmentFilters as $dynamicSegmentFilter) {
$filterData = unserialize($dynamicSegmentFilter['filter_data']);
if (!isset($filterData['product_ids'])) {
$filterData['product_ids'] = [];
}
if (isset($filterData['product_id']) && !in_array($filterData['product_id'], $filterData['product_ids'])) {
$filterData['product_ids'][] = $filterData['product_id'];
unset($filterData['product_id']);
}
if (!isset($filterData['operator'])) {
$filterData['operator'] = DynamicSegmentFilterData::OPERATOR_ANY;
}
$wpdb->update($dynamicSegmentFiltersTable, [
'filter_data' => serialize($filterData),
], ['id' => $dynamicSegmentFilter['id']]);
}
return true;
}
private function migratePurchasedInCategoryDynamicFilters(): bool {
global $wpdb;
// skip the migration if the DB version is higher than 3.75.1 or is not set (a new install)
if (version_compare((string)$this->settings->get('db_version', '3.76.0'), '3.75.1', '>')) {
return false;
}
$dynamicSegmentFiltersTable = esc_sql("{$this->prefix}dynamic_segment_filters");
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE;
$action = WooCommerceCategory::ACTION_CATEGORY;
$dynamicSegmentFilters = $wpdb->get_results($wpdb->prepare("
SELECT `id`, `filter_data`, `filter_type`, `action`
FROM {$dynamicSegmentFiltersTable}
WHERE `filter_type` = %s
AND `action` = %s
", $filterType, $action), ARRAY_A);
foreach ($dynamicSegmentFilters as $dynamicSegmentFilter) {
$filterData = unserialize($dynamicSegmentFilter['filter_data']);
if (!isset($filterData['category_ids'])) {
$filterData['category_ids'] = [];
}
if (isset($filterData['category_id']) && !in_array($filterData['category_id'], $filterData['category_ids'])) {
$filterData['category_ids'][] = $filterData['category_id'];
unset($filterData['category_id']);
}
if (!isset($filterData['operator'])) {
$filterData['operator'] = DynamicSegmentFilterData::OPERATOR_ANY;
}
$wpdb->update($dynamicSegmentFiltersTable, [
'filter_data' => serialize($filterData),
], ['id' => $dynamicSegmentFilter['id']]);
}
return true;
}
private function migrateWooSubscriptionsDynamicFilters(): bool {
global $wpdb;
// skip the migration if the DB version is higher than 3.75.1 or is not set (a new installation)
if (version_compare((string)$this->settings->get('db_version', '3.76.0'), '3.75.1', '>')) {
return false;
}
$dynamicSegmentFiltersTable = esc_sql("{$this->prefix}dynamic_segment_filters");
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION;
$action = WooCommerceSubscription::ACTION_HAS_ACTIVE;
$dynamicSegmentFilters = $wpdb->get_results($wpdb->prepare("
SELECT `id`, `filter_data`, `filter_type`, `action`
FROM {$dynamicSegmentFiltersTable}
WHERE `filter_type` = %s
AND `action` = %s
", $filterType, $action), ARRAY_A);
foreach ($dynamicSegmentFilters as $dynamicSegmentFilter) {
$filterData = unserialize($dynamicSegmentFilter['filter_data']);
if (!isset($filterData['product_ids'])) {
$filterData['product_ids'] = [];
}
if (isset($filterData['product_id']) && !in_array($filterData['product_id'], $filterData['product_ids'])) {
$filterData['product_ids'][] = $filterData['product_id'];
unset($filterData['product_id']);
}
if (!isset($filterData['operator'])) {
$filterData['operator'] = DynamicSegmentFilterData::OPERATOR_ANY;
}
$wpdb->update($dynamicSegmentFiltersTable, [
'filter_data' => serialize($filterData),
], ['id' => $dynamicSegmentFilter['id']]);
}
return true;
}
private function migrateEmailActionsFilters(): bool {
global $wpdb;
// skip the migration if the DB version is higher than 3.77.1 or is not set (a new installation)
if (version_compare($this->settings->get('db_version', '3.77.2'), '3.77.1', '>')) {
return false;
}
$dynamicSegmentFiltersTable = esc_sql("{$this->prefix}dynamic_segment_filters");
$filterType = DynamicSegmentFilterData::TYPE_EMAIL;
$dynamicSegmentFilters = $wpdb->get_results("
SELECT `id`, `filter_data`, `filter_type`, `action`
FROM {$dynamicSegmentFiltersTable}
WHERE `filter_type` = '{$filterType}'
", ARRAY_A);
foreach ($dynamicSegmentFilters as $dynamicSegmentFilter) {
if (!is_array($dynamicSegmentFilter)) {
continue;
}
$filterData = unserialize($dynamicSegmentFilter['filter_data']);
if (!is_array($filterData)) {
continue;
}
$action = $dynamicSegmentFilter['action'];
// Not clicked filter is no longer used and was replaced by clicked with none of operator
if ($action === EmailAction::ACTION_NOT_CLICKED) {
$action = EmailAction::ACTION_CLICKED;
$filterData['operator'] = DynamicSegmentFilterData::OPERATOR_NONE;
}
// Clicked link filter is refactored to work with multiple link ids
if ($action === EmailAction::ACTION_CLICKED) {
if (!isset($filterData['link_ids'])) {
$filterData['link_ids'] = [];
}
if (isset($filterData['link_id']) && !in_array($filterData['link_id'], $filterData['link_ids'])) {
$filterData['link_ids'][] = (int)$filterData['link_id'];
unset($filterData['link_id']);
}
}
// Not opened filter is no longer used and was replaced by opened with none of operand
if ($action === EmailAction::ACTION_NOT_OPENED) {
$action = EmailAction::ACTION_OPENED;
$filterData['operator'] = DynamicSegmentFilterData::OPERATOR_NONE;
}
// Opened and Machine opened filters are refactored to work with multiple newsletters
if (($action === EmailAction::ACTION_OPENED) || ($action === EmailAction::ACTION_MACHINE_OPENED)) {
if (!isset($filterData['newsletters'])) {
$filterData['newsletters'] = [];
}
if (isset($filterData['newsletter_id']) && !in_array($filterData['newsletter_id'], $filterData['newsletters'])) {
$filterData['newsletters'][] = (int)$filterData['newsletter_id'];
unset($filterData['newsletter_id']);
}
}
// Ensure default operator
if (!isset($filterData['operator'])) {
$filterData['operator'] = DynamicSegmentFilterData::OPERATOR_ANY;
}
$wpdb->update($dynamicSegmentFiltersTable, [
'filter_data' => serialize($filterData),
'action' => $action,
], ['id' => $dynamicSegmentFilter['id']]);
}
return true;
}
private function updateDefaultInactiveSubscriberTimeRange(): bool {
// Skip if the installed version is newer than the release that preceded this migration, or if it's a fresh install
$currentlyInstalledVersion = (string)$this->settings->get('db_version', '3.78.1');
if (version_compare($currentlyInstalledVersion, '3.78.0', '>')) {
return false;
}
$currentValue = (int)$this->settings->get('deactivate_subscriber_after_inactive_days');
if ($currentValue === 180) {
$this->settings->set('deactivate_subscriber_after_inactive_days', 365);
$this->settingsChangeHandler->onInactiveSubscribersIntervalChange();
}
return true;
}
private function setDefaultValueForLoadingThirdPartyLibrariesForExistingInstalls(): bool {
// skip the migration if the DB version is higher than 3.91.1 or is not set (a new installation)
if (version_compare($this->settings->get('db_version', '3.91.2'), '3.91.1', '>')) {
return false;
}
$thirdPartyScriptsEnabled = $this->settings->get('3rd_party_libs');
if (is_null($thirdPartyScriptsEnabled)) {
// keep loading 3rd party libraries for existing users so the functionality is not broken
$this->settings->set('3rd_party_libs.enabled', '1');
}
return true;
}
}