This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,38 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
if (!class_exists('\WP_REST_Posts_Controller')) {
require_once ABSPATH . '/wp-includes/rest-api/endpoints/class-wp-rest-controller.php';
require_once ABSPATH . '/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php';
}
class APIPermissionHelper extends \WP_REST_Posts_Controller {
public function __construct() {
// constructor is needed to override parent constructor
}
public function checkReadPermission(\WP_Post $post): bool {
return parent::check_read_permission($post);
}
/**
* Checks if a given post type can be viewed or managed.
* Refrain from checking `show_in_rest` contrary to what parent::check_is_post_type_allowed does
*
* @param \WP_Post_Type|string $post_type Post type name or object.
* @return bool Whether the post type is allowed in REST.
* @see parent::check_is_post_type_allowed
*/
// phpcs:disable PSR1.Methods.CamelCapsMethodName
protected function check_is_post_type_allowed($post_type) {
if (!is_object($post_type)) {
$post_type = get_post_type_object($post_type);
}
return !empty($post_type) && $post_type->public;
}
}
@@ -0,0 +1,23 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
class CdnAssetUrl {
const CDN_URL = 'https://ps.w.org/mailpoet/';
/** @var string */
private $baseUrl;
public function __construct(
string $baseUrl
) {
$this->baseUrl = $baseUrl;
}
public function generateCdnUrl($path) {
$useCdn = defined('MAILPOET_USE_CDN') ? MAILPOET_USE_CDN : true;
return ($useCdn ? self::CDN_URL : $this->baseUrl . '/plugin_repository/') . "assets/$path";
}
}
@@ -0,0 +1,208 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use MailPoet\WP\Functions as WPFunctions;
class ConflictResolver {
public $permittedAssetsLocations = [
'styles' => [
'mailpoet',
// WP default
'^/wp-admin',
'^/wp-includes',
// CDN
'googleapis.com/ajax/libs',
'wp.com',
// third-party
'jetpack',
'query-monitor',
'wpt-tx-updater-network',
// WP.com
'^/_static',
'mu-host-plugins/debug-bar/css',
'woocommerce-payments/',
'automatewoo/',
'full-site-editing',
'wpcomsh',
// Gutenberg
'gutenberg/',
],
'scripts' => [
'mailpoet',
// WP default
'^/wp-admin',
'^/wp-includes',
// CDN
'googleapis.com/ajax/libs',
'wp.com',
// third-party
'query-monitor',
'wpt-tx-updater-network',
// WP.com
'full-site-editing',
'wpcomsh',
],
];
public function init() {
WPFunctions::get()->addAction(
'mailpoet_conflict_resolver_router_url_query_parameters',
[
$this,
'resolveRouterUrlQueryParametersConflict',
]
);
WPFunctions::get()->addAction(
'mailpoet_conflict_resolver_styles',
[
$this,
'resolveStylesConflict',
]
);
WPFunctions::get()->addAction(
'mailpoet_conflict_resolver_scripts',
[
$this,
'resolveScriptsConflict',
]
);
WPFunctions::get()->addAction(
'mailpoet_conflict_resolver_scripts',
[
$this,
'resolveEditorConflict',
]
);
WPFunctions::get()->addAction(
'mailpoet_conflict_resolver_scripts',
[
$this,
'resolveTinyMceConflict',
]
);
}
public function resolveRouterUrlQueryParametersConflict() {
// prevents other plugins from overtaking URL query parameters 'action=' and 'endpoint='
unset($_GET['endpoint'], $_GET['action']);
}
public function resolveStylesConflict() {
$_this = $this;
$_this->permittedAssetsLocations['styles'] = WPFunctions::get()->applyFilters('mailpoet_conflict_resolver_whitelist_style', $_this->permittedAssetsLocations['styles']);
// unload all styles except from the list of allowed
$dequeueStyles = function() use($_this) {
global $wp_styles; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if (!isset($wp_styles->registered)) return; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if (empty($wp_styles->queue)) return; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
foreach ($wp_styles->queue as $wpStyle) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if (empty($wp_styles->registered[$wpStyle])) continue; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$registeredStyle = $wp_styles->registered[$wpStyle]; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if (!is_string($registeredStyle->src)) {
continue;
}
if (!preg_match('!' . implode('|', $_this->permittedAssetsLocations['styles']) . '!i', $registeredStyle->src)) {
WPFunctions::get()->wpDequeueStyle($wpStyle);
}
}
};
// execute last in the following hooks
$executeLast = PHP_INT_MAX;
WPFunctions::get()->addAction('admin_enqueue_scripts', $dequeueStyles, $executeLast); // used also for styles
WPFunctions::get()->addAction('admin_footer', $dequeueStyles, $executeLast);
// execute first in hooks for printing (after printing is too late)
$executeFirst = defined('PHP_INT_MIN') ? constant('PHP_INT_MIN') : ~PHP_INT_MAX;
WPFunctions::get()->addAction('admin_print_styles', $dequeueStyles, $executeFirst);
WPFunctions::get()->addAction('admin_print_footer_scripts', $dequeueStyles, $executeFirst);
}
public function resolveScriptsConflict() {
$_this = $this;
$_this->permittedAssetsLocations['scripts'] = WPFunctions::get()->applyFilters('mailpoet_conflict_resolver_whitelist_script', $_this->permittedAssetsLocations['scripts']);
// unload all scripts except from the list of allowed
$dequeueScripts = function() use($_this) {
global $wp_scripts; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
foreach ($wp_scripts->queue as $wpScript) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if (empty($wp_scripts->registered[$wpScript])) continue; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$registeredScript = $wp_scripts->registered[$wpScript]; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if (!is_string($registeredScript->src)) {
continue;
}
if (!preg_match('!' . implode('|', $_this->permittedAssetsLocations['scripts']) . '!i', $registeredScript->src)) {
WPFunctions::get()->wpDequeueScript($wpScript);
}
}
};
// execute last in the following hooks
$executeLast = PHP_INT_MAX;
WPFunctions::get()->addAction('admin_enqueue_scripts', $dequeueScripts, $executeLast);
WPFunctions::get()->addAction('admin_footer', $dequeueScripts, $executeLast);
// execute first in hooks for printing (after printing is too late)
$executeFirst = defined('PHP_INT_MIN') ? constant('PHP_INT_MIN') : ~PHP_INT_MAX;
WPFunctions::get()->addAction('admin_print_scripts', $dequeueScripts, $executeFirst);
WPFunctions::get()->addAction('admin_print_footer_scripts', $dequeueScripts, $executeFirst);
}
public function resolveEditorConflict() {
// mark editor as already enqueued to prevent loading its assets
// when wp_enqueue_editor() used by some other plugin
global $wp_actions; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$wp_actions['wp_enqueue_editor'] = 1; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
// prevent editor loading when used wp_editor() used by some other plugin
WPFunctions::get()->addFilter('wp_editor_settings', function () {
ob_start();
return [
'tinymce' => false,
'quicktags' => false,
];
});
WPFunctions::get()->addFilter('the_editor', function () {
return '';
});
WPFunctions::get()->addFilter('the_editor_content', function () {
ob_end_clean();
return '';
});
}
public function resolveTinyMceConflict() {
// WordPress TinyMCE scripts may not get enqueued as scripts when some plugins use wp_editor()
// or wp_enqueue_editor(). Instead, they are printed inside the footer script print actions.
// To unload TinyMCE we need to remove those actions.
$tinyMceFooterScriptHooks = [
'_WP_Editors::enqueue_scripts',
'_WP_Editors::editor_js',
'_WP_Editors::force_uncompressed_tinymce',
'_WP_Editors::print_default_editor_scripts',
];
$disableWpTinymce = function() use ($tinyMceFooterScriptHooks) {
global $wp_filter; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$actionName = 'admin_print_footer_scripts';
if (!isset($wp_filter[$actionName])) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
return;
}
foreach ($wp_filter[$actionName]->callbacks as $priority => $callbacks) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
foreach ($tinyMceFooterScriptHooks as $hook) {
if (isset($callbacks[$hook])) {
WPFunctions::get()->removeAction($actionName, $callbacks[$hook]['function'], $priority);
}
}
}
};
WPFunctions::get()->addAction('admin_footer', $disableWpTinymce, PHP_INT_MAX);
}
}
@@ -0,0 +1,52 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use InvalidArgumentException;
class Cookies {
const DEFAULT_OPTIONS = [
'expires' => 0,
'path' => '',
'domain' => '',
'secure' => false,
'httponly' => false,
];
public function set($name, $value, array $options = []) {
if (headers_sent()) {
return;
}
$options = $options + self::DEFAULT_OPTIONS;
$value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$error = json_last_error();
if ($error || ($value === false)) {
throw new InvalidArgumentException();
}
setcookie(
$name,
$value,
$options
);
}
public function get($name) {
if (!array_key_exists($name, $_COOKIE)) {
return null;
}
$value = json_decode(sanitize_text_field(wp_unslash(($_COOKIE[$name]))), true);
$error = json_last_error();
if ($error) {
return null;
}
return $value;
}
public function delete($name) {
unset($_COOKIE[$name]);
}
}
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class DBCollationChecker {
/** @var EntityManager */
private $entityManager;
public function __construct(
EntityManager $entityManager
) {
$this->entityManager = $entityManager;
}
/**
* If two columns have incompatible collations returns MySQL's COLLATE command to be used with the target table column.
* e.g. WHERE source_table.column = target_table.column COLLATE xyz
*
* In MySQL, if you have the same charset and collation in joined tables' columns it's perfect;
* if you have different charsets, utf8 and utf8mb4, it works too; but if you have the same charset
* with different collations, e.g. utf8mb4_unicode_ci and utf8mb4_unicode_520_ci, it will fail
* with an 'Illegal mix of collations' error.
*/
public function getCollateIfNeeded(string $sourceTable, string $sourceColumn, string $targetTable, string $targetColumn): string {
$connection = $this->entityManager->getConnection();
$sourceColumnData = $connection->executeQuery("SHOW FULL COLUMNS FROM $sourceTable WHERE Field = '$sourceColumn';")->fetchAllAssociative();
$sourceCollation = $sourceColumnData[0]['Collation'] ?? '';
$targetColumnData = $connection->executeQuery("SHOW FULL COLUMNS FROM $targetTable WHERE Field = '$targetColumn';")->fetchAllAssociative();
$targetCollation = $targetColumnData[0]['Collation'] ?? '';
if ($sourceCollation === $targetCollation) {
return '';
}
list($sourceCharset) = explode('_', $sourceCollation);
list($targetCharset) = explode('_', $targetCollation);
if ($sourceCharset === $targetCharset) {
return "COLLATE $sourceCollation";
}
return '';
}
}
@@ -0,0 +1,44 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\pQuery\DomNode;
class DOM {
/**
* Splits a DOM tree around the cut element, bringing it up to bound
* ancestor and splitting left and right siblings into subtrees along
* the way, retaining order and nesting level.
*/
public static function splitOn(DomNode $bound, DomNode $cutElement) {
$ignoreTextAndCommentNodes = false;
$grandparent = $cutElement->parent;
for ($parent = $cutElement->parent; $bound != $parent; $parent = $grandparent) {
// Clone parent node without children, but with attributes
$parent->after($parent->toString());
$right = $parent->getNextSibling($ignoreTextAndCommentNodes);
$right->clear();
while ($sibling = $cutElement->getNextSibling($ignoreTextAndCommentNodes)) {
$sibling->move($right);
}
// Reattach cut_element and right siblings to grandparent
$grandparent = $parent->parent;
$indexAfterParent = $parent->index() + 1;
$right->move($grandparent, $indexAfterParent);
$indexAfterParent = $parent->index() + 1;
$cutElement->move($grandparent, $indexAfterParent);
}
}
public static function findTopAncestor(DomNode $item) {
while ($item->parent->parent !== null) {
$item = $item->parent;
}
return $item;
}
}
@@ -0,0 +1,66 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\DataInconsistency;
if (!defined('ABSPATH')) exit;
use MailPoet\UnexpectedValueException;
class DataInconsistencyController {
const ORPHANED_SENDING_TASKS = 'orphaned_sending_tasks';
const ORPHANED_SENDING_TASK_SUBSCRIBERS = 'orphaned_sending_task_subscribers';
const SENDING_QUEUE_WITHOUT_NEWSLETTER = 'sending_queue_without_newsletter';
const ORPHANED_SUBSCRIPTIONS = 'orphaned_subscriptions';
const ORPHANED_LINKS = 'orphaned_links';
const ORPHANED_NEWSLETTER_POSTS = 'orphaned_newsletter_posts';
const SUPPORTED_INCONSISTENCY_CHECKS = [
self::ORPHANED_SENDING_TASKS,
self::ORPHANED_SENDING_TASK_SUBSCRIBERS,
self::SENDING_QUEUE_WITHOUT_NEWSLETTER,
self::ORPHANED_SUBSCRIPTIONS,
self::ORPHANED_LINKS,
self::ORPHANED_NEWSLETTER_POSTS,
];
private DataInconsistencyRepository $repository;
public function __construct(
DataInconsistencyRepository $repository
) {
$this->repository = $repository;
}
public function getInconsistentDataStatus(): array {
$result = [
self::ORPHANED_SENDING_TASKS => $this->repository->getOrphanedSendingTasksCount(),
self::ORPHANED_SENDING_TASK_SUBSCRIBERS => $this->repository->getOrphanedScheduledTasksSubscribersCount(),
self::SENDING_QUEUE_WITHOUT_NEWSLETTER => $this->repository->getSendingQueuesWithoutNewsletterCount(),
self::ORPHANED_SUBSCRIPTIONS => $this->repository->getOrphanedSubscriptionsCount(),
self::ORPHANED_LINKS => $this->repository->getOrphanedNewsletterLinksCount(),
self::ORPHANED_NEWSLETTER_POSTS => $this->repository->getOrphanedNewsletterPostsCount(),
];
$result['total'] = array_sum($result);
return $result;
}
public function fixInconsistentData(string $inconsistency): void {
if (!in_array($inconsistency, self::SUPPORTED_INCONSISTENCY_CHECKS, true)) {
throw new UnexpectedValueException(__('Unsupported data inconsistency check.', 'mailpoet'));
}
if ($inconsistency === self::ORPHANED_SENDING_TASKS) {
$this->repository->cleanupOrphanedSendingTasks();
} elseif ($inconsistency === self::ORPHANED_SENDING_TASK_SUBSCRIBERS) {
$this->repository->cleanupOrphanedScheduledTaskSubscribers();
} elseif ($inconsistency === self::SENDING_QUEUE_WITHOUT_NEWSLETTER) {
$this->repository->cleanupSendingQueuesWithoutNewsletter();
} elseif ($inconsistency === self::ORPHANED_SUBSCRIPTIONS) {
$this->repository->cleanupOrphanedSubscriptions();
} elseif ($inconsistency === self::ORPHANED_LINKS) {
$this->repository->cleanupOrphanedNewsletterLinks();
} elseif ($inconsistency === self::ORPHANED_NEWSLETTER_POSTS) {
$this->repository->cleanupOrphanedNewsletterPosts();
}
}
}
@@ -0,0 +1,256 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\DataInconsistency;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterLinkEntity;
use MailPoet\Entities\NewsletterPostEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
use MailPoetVendor\Doctrine\DBAL\ParameterType;
use MailPoetVendor\Doctrine\ORM\EntityManager;
use MailPoetVendor\Doctrine\ORM\Query;
use MailPoetVendor\Doctrine\ORM\QueryBuilder;
class DataInconsistencyRepository {
const DELETE_ROWS_LIMIT = 10000;
private EntityManager $entityManager;
public function __construct(
EntityManager $entityManager
) {
$this->entityManager = $entityManager;
}
public function getOrphanedSendingTasksCount(): int {
$builder = $this->entityManager->createQueryBuilder()
->select('count(st.id)');
return (int)$this->buildOrphanedSendingTasksQuery($builder)->getSingleScalarResult();
}
public function getOrphanedScheduledTasksSubscribersCount(): int {
$this->createOrphanedScheduledTaskSubscribersTemporaryTables();
$count = $this->getOrphanedScheduledTasksSubscribersCountFromTemporaryTables();
$this->dropOrphanedScheduledTaskSubscribersTemporaryTables();
return $count;
}
private function getOrphanedScheduledTasksSubscribersCountFromTemporaryTables(): int {
$connection = $this->entityManager->getConnection();
$stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
/** @var string $count */
$count = $connection->executeQuery("
SELECT COUNT(*) FROM $stsTable sts WHERE sts.task_id IN (SELECT task_id FROM orphaned_task_ids)
")->fetchOne();
return intval($count);
}
public function getSendingQueuesWithoutNewsletterCount(): int {
$sqTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
/** @var string $count */
$count = $this->entityManager->getConnection()->executeQuery("
SELECT count(*) FROM $sqTable sq
LEFT JOIN $newsletterTable n ON n.`id` = sq.`newsletter_id`
WHERE n.`id` IS NULL
")->fetchOne();
return intval($count);
}
public function getOrphanedSubscriptionsCount(): int {
$subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$segmentTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
/** @var string $count */
$count = $this->entityManager->getConnection()->executeQuery("
SELECT count(distinct ss.`id`) FROM $subscriberSegmentTable ss
LEFT JOIN $segmentTable seg ON seg.`id` = ss.`segment_id`
LEFT JOIN $subscriberTable sub ON sub.`id` = ss.`subscriber_id`
WHERE seg.`id` IS NULL OR sub.`id` IS NULL
")->fetchOne();
return intval($count);
}
public function getOrphanedNewsletterLinksCount(): int {
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
$sendingQueueTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
$newsletterLinkTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
/** @var string $count */
$count = $this->entityManager->getConnection()->executeQuery("
SELECT count(distinct nl.`id`) FROM $newsletterLinkTable nl
LEFT JOIN $newsletterTable n ON n.`id` = nl.`newsletter_id`
LEFT JOIN $sendingQueueTable sq ON sq.`id` = nl.`queue_id`
WHERE n.`id` IS NULL OR sq.`id` IS NULL
")->fetchOne();
return intval($count);
}
public function getOrphanedNewsletterPostsCount(): int {
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
$newsletterPostTable = $this->entityManager->getClassMetadata(NewsletterPostEntity::class)->getTableName();
/** @var string $count */
$count = $this->entityManager->getConnection()->executeQuery("
SELECT count(distinct np.`id`) FROM $newsletterPostTable np
LEFT JOIN $newsletterTable n ON n.`id` = np.`newsletter_id`
WHERE n.`id` IS NULL
")->fetchOne();
return intval($count);
}
public function cleanupOrphanedSendingTasks(): int {
/** @var array<int, array{id: string}> $ids */
$ids = $this->buildOrphanedSendingTasksQuery(
$this->entityManager->createQueryBuilder()
->select('st.id')
)->getResult();
if (!$ids) {
return 0;
}
$ids = array_column($ids, 'id');
// delete the orphaned tasks
$qb = $this->entityManager->createQueryBuilder();
$countDeletedTasks = $qb->delete(ScheduledTaskEntity::class, 'st')
->where($qb->expr()->in('st.id', ':ids'))
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete the scheduled tasks subscribers
$stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
$this->entityManager->getConnection()->executeStatement(
"DELETE sts_top FROM $stsTable sts_top
JOIN (
SELECT sts.`task_id`, sts.`subscriber_id` FROM $stsTable sts
WHERE `task_id` IN (:ids)
LIMIT :limit
) as to_delete ON sts_top.`task_id` = to_delete.`task_id` AND sts_top.`subscriber_id` = to_delete.`subscriber_id`",
['limit' => self::DELETE_ROWS_LIMIT, 'ids' => $ids],
['limit' => ParameterType::INTEGER, 'ids' => ArrayParameterType::INTEGER]
);
$qb = $this->entityManager->createQueryBuilder();
$qb->delete(ScheduledTaskSubscriberEntity::class, 'sts')
->where($qb->expr()->in('sts.task', ':ids'))
->setParameter('ids', $ids)
->getQuery()
->execute();
return $countDeletedTasks;
}
public function cleanupOrphanedScheduledTaskSubscribers(): int {
$stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
$deletedCount = 0;
$this->createOrphanedScheduledTaskSubscribersTemporaryTables();
do {
$deletedCount += (int)$this->entityManager->getConnection()->executeStatement(
"
DELETE sts_top FROM $stsTable sts_top
JOIN (
SELECT task_id, subscriber_id
FROM $stsTable
WHERE task_id IN (SELECT task_id FROM orphaned_task_ids)
LIMIT :limit
) AS to_delete ON sts_top.task_id = to_delete.task_id AND sts_top.subscriber_id = to_delete.subscriber_id
",
['limit' => self::DELETE_ROWS_LIMIT],
['limit' => ParameterType::INTEGER]
);
} while ($this->getOrphanedScheduledTasksSubscribersCountFromTemporaryTables() > 0);
$this->dropOrphanedScheduledTaskSubscribersTemporaryTables();
return $deletedCount;
}
public function cleanupSendingQueuesWithoutNewsletter(): int {
$sqTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
$deletedQueuesCount = (int)$this->entityManager->getConnection()->executeStatement("
DELETE sq FROM $sqTable sq
LEFT JOIN $newsletterTable n ON n.`id` = sq.`newsletter_id`
WHERE n.`id` IS NULL
");
$this->cleanupOrphanedSendingTasks();
return $deletedQueuesCount;
}
public function cleanupOrphanedSubscriptions(): int {
$subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$segmentTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
return (int)$this->entityManager->getConnection()->executeStatement("
DELETE ss FROM $subscriberSegmentTable ss
LEFT JOIN $segmentTable seg ON seg.`id` = ss.`segment_id`
LEFT JOIN $subscriberTable sub ON sub.`id` = ss.`subscriber_id`
WHERE seg.`id` IS NULL OR sub.`id` IS NULL
");
}
public function cleanupOrphanedNewsletterLinks(): int {
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
$sendingQueueTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
$newsletterLinkTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
return (int)$this->entityManager->getConnection()->executeStatement("
DELETE nl FROM $newsletterLinkTable nl
LEFT JOIN $newsletterTable n ON n.`id` = nl.`newsletter_id`
LEFT JOIN $sendingQueueTable sq ON sq.`id` = nl.`queue_id`
WHERE n.`id` IS NULL OR sq.`id` IS NULL
");
}
public function cleanupOrphanedNewsletterPosts(): int {
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
$newsletterPostTable = $this->entityManager->getClassMetadata(NewsletterPostEntity::class)->getTableName();
return (int)$this->entityManager->getConnection()->executeStatement("
DELETE np FROM $newsletterPostTable np
LEFT JOIN $newsletterTable n ON n.`id` = np.`newsletter_id`
WHERE n.`id` IS NULL
");
}
private function buildOrphanedSendingTasksQuery(QueryBuilder $queryBuilder): Query {
return $queryBuilder
->from(ScheduledTaskEntity::class, 'st')
->leftJoin('st.sendingQueue', 'sq')
->where('sq.id IS NULL')
->andWhere('st.type = :type')
->setParameter('type', SendingQueue::TASK_TYPE)
->getQuery();
}
private function createOrphanedScheduledTaskSubscribersTemporaryTables(): void {
$connection = $this->entityManager->getConnection();
$stTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
$stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
// 1. Get the DISTINCT task IDs so that the subsequent JOIN is more efficient.
$connection->executeStatement("
CREATE TEMPORARY TABLE IF NOT EXISTS task_ids
SELECT DISTINCT task_id FROM $stsTable
");
// 2. Get the orphaned task IDs.
$connection->executeStatement("
CREATE TEMPORARY TABLE IF NOT EXISTS orphaned_task_ids
SELECT task_id FROM task_ids LEFT JOIN $stTable st ON st.id = task_ids.task_id WHERE st.id IS NULL
");
}
private function dropOrphanedScheduledTaskSubscribersTemporaryTables(): void {
$this->entityManager->getConnection()->executeStatement("DROP TEMPORARY TABLE IF EXISTS task_ids");
$this->entityManager->getConnection()->executeStatement("DROP TEMPORARY TABLE IF EXISTS orphaned_task_ids");
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,109 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Carbon\Carbon;
class DateConverter {
/**
* @return string|false
*/
public function convertDateToDatetime(string $date, string $dateFormat) {
$datetime = false;
if ($dateFormat === 'datetime') {
$datetime = $date;
} elseif ($dateFormat === 'd/m/Y') {
$datetime = str_replace('/', '-', $date);
} else {
$parsedDate = explode('/', $date);
$parsedDateFormat = explode('/', $dateFormat);
$yearPosition = array_search('YYYY', $parsedDateFormat);
$monthPosition = array_search('MM', $parsedDateFormat);
$dayPosition = array_search('DD', $parsedDateFormat);
if (count($parsedDate) === 3) {
// create date from any combination of month, day and year
$parsedDate = [
'year' => $parsedDate[$yearPosition],
'month' => $parsedDate[$monthPosition],
'day' => $parsedDate[$dayPosition],
];
} else if (count($parsedDate) === 2) {
// create date from any combination of month and year
$parsedDate = [
'year' => $parsedDate[$yearPosition],
'month' => $parsedDate[$monthPosition],
'day' => '01',
];
} else if ($dateFormat === 'MM' && count($parsedDate) === 1) {
// create date from month
if ((int)$parsedDate[$monthPosition] === 0) {
$datetime = '';
$parsedDate = false;
} else {
$parsedDate = [
'month' => $parsedDate[$monthPosition],
'day' => '01',
'year' => date('Y'),
];
}
} else if ($dateFormat === 'YYYY' && count($parsedDate) === 1) {
// create date from year
if ((int)$parsedDate[$yearPosition] === 0) {
$datetime = '';
$parsedDate = false;
} else {
$parsedDate = [
'year' => $parsedDate[$yearPosition],
'month' => '01',
'day' => '01',
];
}
} else if ($dateFormat === 'DD' && count($parsedDate) === 1) {
// create date from day
if ((int)$parsedDate[$dayPosition] === 0) {
$datetime = '';
$parsedDate = false;
} else {
$parsedDate = [
'year' => date('Y'),
'month' => '01',
'day' => $parsedDate[$dayPosition],
];
}
} else {
$parsedDate = false;
}
if ($parsedDate) {
$year = $parsedDate['year'];
$month = $parsedDate['month'];
$day = $parsedDate['day'];
// if all date parts are set to 0, date value is empty
if ((int)$year === 0 && (int)$month === 0 && (int)$day === 0) {
$datetime = '';
} else {
if ((int)$year === 0) $year = date('Y');
if ((int)$month === 0) $month = date('m');
if ((int)$day === 0) $day = date('d');
$datetime = sprintf(
'%s-%s-%s 00:00:00',
$year,
$month,
$day
);
}
}
}
if ($datetime !== false && !empty($datetime)) {
try {
$datetime = Carbon::parse($datetime)->toDateTimeString();
} catch (\Exception $e) {
$datetime = false;
}
}
return $datetime;
}
}
@@ -0,0 +1,67 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
class FreeDomains {
/* https://github.com/mailcheck/mailcheck/wiki/List-of-Popular-Domains */
const FREE_DOMAINS = [
/* Default domains included */
'aol.com', 'att.net', 'comcast.net', 'facebook.com', 'gmail.com', 'gmx.com', 'googlemail.com',
'google.com', 'hotmail.com', 'hotmail.co.uk', 'mac.com', 'me.com', 'mail.com', 'msn.com',
'live.com', 'sbcglobal.net', 'verizon.net', 'yahoo.com', 'yahoo.co.uk',
/* Other global domains */
'email.com', 'fastmail.fm', 'games.com' /* AOL */, 'gmx.net', 'hush.com', 'hushmail.com', 'icloud.com',
'iname.com', 'inbox.com', 'lavabit.com', 'love.com' /* AOL */, 'outlook.com', 'pobox.com', 'protonmail.ch', 'protonmail.com', 'tutanota.de', 'tutanota.com', 'tutamail.com', 'tuta.io',
'keemail.me', 'rocketmail.com' /* Yahoo */, 'safe-mail.net', 'wow.com' /* AOL */, 'ygm.com' /* AOL */,
'ymail.com' /* Yahoo */, 'zoho.com', 'yandex.com',
/* United States ISP domains */
'bellsouth.net', 'charter.net', 'cox.net', 'earthlink.net', 'juno.com',
/* British ISP domains */
'btinternet.com', 'virginmedia.com', 'blueyonder.co.uk', 'live.co.uk',
'ntlworld.com', 'orange.net', 'sky.com', 'talktalk.co.uk', 'tiscali.co.uk',
'virgin.net', 'bt.com',
/* Domains used in Asia */
'sina.com', 'sina.cn', 'qq.com', 'naver.com', 'hanmail.net', 'daum.net', 'nate.com', 'yahoo.co.jp', 'yahoo.co.kr', 'yahoo.co.id', 'yahoo.co.in', 'yahoo.com.sg', 'yahoo.com.ph', '163.com', 'yeah.net', '126.com', '21cn.com', 'aliyun.com', 'foxmail.com',
/* French ISP domains */
'hotmail.fr', 'live.fr', 'laposte.net', 'yahoo.fr', 'wanadoo.fr', 'orange.fr', 'gmx.fr', 'sfr.fr', 'neuf.fr', 'free.fr',
/* German ISP domains */
'gmx.de', 'hotmail.de', 'live.de', 'online.de', 't-online.de' /* T-Mobile */, 'web.de', 'yahoo.de',
/* Italian ISP domains */
'libero.it', 'virgilio.it', 'hotmail.it', 'aol.it', 'tiscali.it', 'alice.it', 'live.it', 'yahoo.it', 'email.it', 'tin.it', 'poste.it', 'teletu.it',
/* Russian ISP domains */
'bk.ru', 'inbox.ru', 'list.ru', 'mail.ru', 'rambler.ru', 'yandex.by', 'yandex.com', 'yandex.kz', 'yandex.ru', 'yandex.ua', 'ya.ru',
/* Belgian ISP domains */
'hotmail.be', 'live.be', 'skynet.be', 'voo.be', 'tvcablenet.be', 'telenet.be',
/* Argentinian ISP domains */
'hotmail.com.ar', 'live.com.ar', 'yahoo.com.ar', 'fibertel.com.ar', 'speedy.com.ar', 'arnet.com.ar',
/* Domains used in Mexico */
'yahoo.com.mx', 'live.com.mx', 'hotmail.es', 'hotmail.com.mx', 'prodigy.net.mx',
/* Domains used in Canada */
'yahoo.ca', 'hotmail.ca', 'bell.net', 'shaw.ca', 'sympatico.ca', 'rogers.com',
/* Domains used in Brazil */
'yahoo.com.br', 'hotmail.com.br', 'outlook.com.br', 'uol.com.br', 'bol.com.br', 'terra.com.br', 'ig.com.br', 'r7.com', 'zipmail.com.br', 'globo.com', 'globomail.com', 'oi.com.br',
];
public function isEmailOnFreeDomain($email) {
$emailParts = explode('@', $email);
$domain = end($emailParts);
return in_array($domain, self::FREE_DOMAINS);
}
}
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use MailPoet\WP\Functions as WPFunctions;
class Headers {
public static function setNoCacheHeaders(): void {
$wp = WPFunctions::get();
if ($wp->headersSent()) {
return;
}
// Set default no-cache headers:
header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1+
header('Pragma: no-cache'); // HTTP 1.0
header('Expires: 0'); // proxies
header('X-Cache-Enabled: False'); // SG Optimizer on SiteGround
header('X-LiteSpeed-Cache-Control: no-cache'); // LiteSpeed server
// Use WP-native nocache_headers(). This can override the defaults above.
$wp->nocacheHeaders();
}
}
@@ -0,0 +1,147 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
class Helpers {
const DIVIDER = '***MailPoet***';
const LINK_TAG = 'link';
public static function isJson($string) {
if (!is_string($string)) return false;
json_decode($string);
return json_last_error() == JSON_ERROR_NONE;
}
public static function replaceLinkTags(
string $source,
string $link,
array $attributes = [],
string $linkTag = self::LINK_TAG
) {
if (empty($link)) return $source;
$attributes = array_map(function($key) use ($attributes) {
return sprintf('%s="%s"', $key, $attributes[$key]);
}, array_keys($attributes));
$source = str_replace(
'[' . $linkTag . ']',
sprintf(
'<a %s href="%s">',
join(' ', $attributes),
$link
),
$source
);
$source = str_replace('[/' . $linkTag . ']', '</a>', $source);
return preg_replace('/\s+/', ' ', $source);
}
public static function getMaxPostSize($bytes = false) {
$maxPostSize = ini_get('post_max_size');
if (!$bytes) return $maxPostSize;
if ($maxPostSize === false) {
return 0;
}
switch (substr($maxPostSize, -1)) {
case 'M':
case 'm':
return (int)$maxPostSize * 1048576;
case 'K':
case 'k':
return (int)$maxPostSize * 1024;
case 'G':
case 'g':
return (int)$maxPostSize * 1073741824;
default:
return $maxPostSize;
}
}
public static function flattenArray($array) {
if (!$array) return;
$flattenedArray = [];
array_walk_recursive($array, function ($a) use (&$flattenedArray) {
$flattenedArray[] = $a;
});
return $flattenedArray;
}
public static function underscoreToCamelCase($str, $capitaliseFirstChar = false) {
if ($capitaliseFirstChar) {
$str[0] = strtoupper($str[0]);
}
return preg_replace_callback('/_([a-z])/', function ($c) {
return strtoupper($c[1]);
}, $str);
}
public static function camelCaseToUnderscore($str) {
$str[0] = strtolower($str[0]);
return preg_replace_callback('/([A-Z])/', function ($c) {
return "_" . strtolower($c[1]);
}, $str);
}
public static function camelCaseToKebabCase($str) {
$str[0] = strtolower($str[0]);
return preg_replace_callback('/([A-Z])/', function ($c) {
return "-" . strtolower($c[1]);
}, $str);
}
public static function joinObject($object = []) {
return implode(self::DIVIDER, $object);
}
public static function splitObject($object = []) {
return explode(self::DIVIDER, $object);
}
public static function getIP() {
return (isset($_SERVER['REMOTE_ADDR']))
? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))
: null;
}
public static function recursiveTrim($value) {
if (is_array($value))
return array_map([__CLASS__, 'recursiveTrim'], $value);
if (is_string($value))
return trim($value);
return $value;
}
public static function escapeSearch(string $search): string {
return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], trim($search)); // escape for 'LIKE'
}
public static function extractEmailDomain(string $email = ''): string {
$arrayOfItems = explode('@', trim($email));
return strtolower(array_pop($arrayOfItems));
}
public static function mySqlGoneAwayExceptionHandler(\Throwable $err): string {
$errorMessage = $err->getMessage() ? $err->getMessage() : '';
$mySqlGoneAwayCheck = strpos(strtolower($errorMessage), 'mysql server has gone away') !== false;
if ($mySqlGoneAwayCheck) {
$customErrorMessage = sprintf(
// translators: the %1$s is the link, the %2$s is the error message.
__('Please see %1$s for more information. %2$s.', 'mailpoet'),
'https://kb.mailpoet.com/article/307-how-to-fix-general-error-2006-mysql-server-has-gone-away',
$errorMessage
);
// logging to the php log
if (function_exists('error_log')) {
error_log($customErrorMessage); // phpcs:ignore Squiz.PHP.DiscouragedFunctions
}
return $customErrorMessage;
}
return '';
}
}
@@ -0,0 +1,32 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use MailPoet\Settings\SettingsController;
use MailPoetVendor\Carbon\Carbon;
class Installation {
const NEW_INSTALLATION_DAYS_LIMIT = 30;
/** @var SettingsController */
private $settings;
public function __construct(
SettingsController $settings
) {
$this->settings = $settings;
}
public function isNewInstallation() {
$installedAt = $this->settings->get('installed_at');
if (is_null($installedAt)) {
return true;
}
$installedAt = Carbon::createFromTimestamp(strtotime($installedAt));
$currentTime = Carbon::now()->millisecond(0);
return $currentTime->diffInDays($installedAt) <= self::NEW_INSTALLATION_DAYS_LIMIT;
}
}
@@ -0,0 +1,84 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Env;
/**
* These constants for table names were used with the idiorm ORM library that we removed.
* They are kept here for sometime for back compatibility with extensions that may still use them.
*
* PHP doesn't have a built-in support for deprecation of constants defined with define() function.
* But some IDEs like PHPStorm can recognize the @deprecated annotation and show warnings.
*
* We will remove them after January 2025.
*/
class LegacyDatabase {
public static function defineTableConstants() {
if (!defined('MP_SETTINGS_TABLE')) {
/** @deprecated */
define('MP_SETTINGS_TABLE', Env::$dbPrefix . 'settings');
/** @deprecated */
define('MP_SEGMENTS_TABLE', Env::$dbPrefix . 'segments');
/** @deprecated */
define('MP_FORMS_TABLE', Env::$dbPrefix . 'forms');
/** @deprecated */
define('MP_CUSTOM_FIELDS_TABLE', Env::$dbPrefix . 'custom_fields');
/** @deprecated */
define('MP_SUBSCRIBERS_TABLE', Env::$dbPrefix . 'subscribers');
/** @deprecated */
define('MP_SUBSCRIBER_SEGMENT_TABLE', Env::$dbPrefix . 'subscriber_segment');
/** @deprecated */
define('MP_SUBSCRIBER_CUSTOM_FIELD_TABLE', Env::$dbPrefix . 'subscriber_custom_field');
/** @deprecated */
define('MP_SUBSCRIBER_IPS_TABLE', Env::$dbPrefix . 'subscriber_ips');
/** @deprecated */
define('MP_NEWSLETTER_SEGMENT_TABLE', Env::$dbPrefix . 'newsletter_segment');
/** @deprecated */
define('MP_SCHEDULED_TASKS_TABLE', Env::$dbPrefix . 'scheduled_tasks');
/** @deprecated */
define('MP_SCHEDULED_TASK_SUBSCRIBERS_TABLE', Env::$dbPrefix . 'scheduled_task_subscribers');
/** @deprecated */
define('MP_SENDING_QUEUES_TABLE', Env::$dbPrefix . 'sending_queues');
/** @deprecated */
define('MP_NEWSLETTERS_TABLE', Env::$dbPrefix . 'newsletters');
/** @deprecated */
define('MP_NEWSLETTER_TEMPLATES_TABLE', Env::$dbPrefix . 'newsletter_templates');
/** @deprecated */
define('MP_NEWSLETTER_OPTION_FIELDS_TABLE', Env::$dbPrefix . 'newsletter_option_fields');
/** @deprecated */
define('MP_NEWSLETTER_OPTION_TABLE', Env::$dbPrefix . 'newsletter_option');
/** @deprecated */
define('MP_NEWSLETTER_LINKS_TABLE', Env::$dbPrefix . 'newsletter_links');
/** @deprecated */
define('MP_NEWSLETTER_POSTS_TABLE', Env::$dbPrefix . 'newsletter_posts');
/** @deprecated */
define('MP_STATISTICS_NEWSLETTERS_TABLE', Env::$dbPrefix . 'statistics_newsletters');
/** @deprecated */
define('MP_STATISTICS_CLICKS_TABLE', Env::$dbPrefix . 'statistics_clicks');
/** @deprecated */
define('MP_STATISTICS_OPENS_TABLE', Env::$dbPrefix . 'statistics_opens');
/** @deprecated */
define('MP_STATISTICS_UNSUBSCRIBES_TABLE', Env::$dbPrefix . 'statistics_unsubscribes');
/** @deprecated */
define('MP_STATISTICS_FORMS_TABLE', Env::$dbPrefix . 'statistics_forms');
/** @deprecated */
define('MP_STATISTICS_WOOCOMMERCE_PURCHASES_TABLE', Env::$dbPrefix . 'statistics_woocommerce_purchases');
/** @deprecated */
define('MP_MAPPING_TO_EXTERNAL_ENTITIES_TABLE', Env::$dbPrefix . 'mapping_to_external_entities');
/** @deprecated */
define('MP_LOG_TABLE', Env::$dbPrefix . 'log');
/** @deprecated */
define('MP_STATS_NOTIFICATIONS_TABLE', Env::$dbPrefix . 'stats_notifications');
/** @deprecated */
define('MP_USER_FLAGS_TABLE', Env::$dbPrefix . 'user_flags');
/** @deprecated */
define('MP_FEATURE_FLAGS_TABLE', Env::$dbPrefix . 'feature_flags');
/** @deprecated */
define('MP_DYNAMIC_SEGMENTS_FILTERS_TABLE', Env::$dbPrefix . 'dynamic_segment_filters');
}
}
}
@@ -0,0 +1,163 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\License\Features;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\ServicesChecker;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\License\Features\Data\Capability;
class CapabilitiesManager {
// Settings mapping
const MSS_TIER_SETTING_KEY = 'mta.mailpoet_api_key_state.data.tier';
const MSS_MAILPOET_LOGO_IN_EMAILS_SETTING_KEY = 'mta.mailpoet_api_key_state.data.mailpoet_logo_in_emails';
const MSS_DETAILED_ANALYTICS_SETTING_KEY = 'mta.mailpoet_api_key_state.data.detailed_analytics';
const MSS_AUTOMATION_STEPS_SETTING_KEY = 'mta.mailpoet_api_key_state.data.automation_steps';
const MSS_SEGMENT_FILTERS_SETTING_KEY = 'mta.mailpoet_api_key_state.data.segment_filters';
// Product capabilities mapping
const MIN_TIER_LOGO_NOT_REQUIRED = 1;
const MIN_TIER_ANALYTICS_ENABLED = 1;
const MIN_TIER_NO_UPGRADE_PAGE = 2;
const MIN_TIER_UNLIMITED_AUTOMATION_STEPS = 2;
const MIN_TIER_UNLIMITED_SEGMENT_FILTERS = 2;
private SettingsController $settings;
private ServicesChecker $servicesChecker;
private Subscribers $subscribersFeature;
private ?int $tier;
private bool $isKeyValid = false;
/** @var array<string,Capability>|null */
private ?array $capabilities = null;
public function __construct(
SettingsController $settings,
ServicesChecker $servicesChecker,
Subscribers $subscribersFeature
) {
$this->settings = $settings;
$this->servicesChecker = $servicesChecker;
$this->subscribersFeature = $subscribersFeature;
}
public function getTier(): ?int {
$tier = $this->settings->get(self::MSS_TIER_SETTING_KEY);
return isset($tier) ? (int)$tier : null;
}
private function isMailpoetLogoInEmailsRequired(): bool {
$mailpoetLogoInEmails = $this->settings->get(self::MSS_MAILPOET_LOGO_IN_EMAILS_SETTING_KEY);
if (!isset($this->tier) && !isset($mailpoetLogoInEmails)) {
return !$this->servicesChecker->isUserActivelyPaying(); // Backward compatibility
}
if (!$this->isKeyValid) {
return true;
}
// Allow for less restrictive individual capability to take precedence over tier
if (isset($mailpoetLogoInEmails) && (bool)$mailpoetLogoInEmails === false) {
return false;
}
return !isset($this->tier) || $this->tier < self::MIN_TIER_LOGO_NOT_REQUIRED;
}
private function isDetailedAnalyticsEnabled(): bool {
// Preconditions
if (!$this->subscribersFeature->hasValidPremiumKey() || $this->subscribersFeature->check() || !$this->servicesChecker->isPremiumPluginActive()) {
return false;
}
$detailedAnalytics = $this->settings->get(self::MSS_DETAILED_ANALYTICS_SETTING_KEY);
if (!isset($this->tier) && !isset($detailedAnalytics)) {
return true; // Backward compatibility is true when preconditions have been met
}
// Allow for less restrictive individual capability to take precedence
if (isset($detailedAnalytics) && (bool)$detailedAnalytics === true) {
return true;
}
return (isset($this->tier) && $this->tier >= self::MIN_TIER_ANALYTICS_ENABLED);
}
private function getLimit(string $settingKey, int $minTierForUnlimited): int {
$capabilityValue = $this->settings->get($settingKey);
if (!isset($this->tier) && !isset($capabilityValue)) {
return 0; // Backward compatibility
}
$limitFromTier = isset($this->tier) && $this->tier >= $minTierForUnlimited ? 0 : 1; // 0 is unlimited
if ($limitFromTier === 0) {
return 0;
}
// Allow for less restrictive individual capability to take precedence
return (isset($capabilityValue) && ((int)$capabilityValue === 0 || (int)$capabilityValue > $limitFromTier)) ? (int)$capabilityValue : $limitFromTier;
}
private function getAutomationStepsLimit(): int {
return $this->getLimit(self::MSS_AUTOMATION_STEPS_SETTING_KEY, self::MIN_TIER_UNLIMITED_AUTOMATION_STEPS);
}
private function getSegmentFiltersLimit(): int {
return $this->getLimit(self::MSS_SEGMENT_FILTERS_SETTING_KEY, self::MIN_TIER_UNLIMITED_SEGMENT_FILTERS);
}
public function initCapabilities(): void {
$this->tier = $this->getTier();
$isPremiumKeyValid = $this->servicesChecker->isPremiumKeyValid(false);
$this->isKeyValid = $isPremiumKeyValid || $this->servicesChecker->isMailPoetAPIKeyValid(false);
$automationSteps = $this->getAutomationStepsLimit();
$segmentFilters = $this->getSegmentFiltersLimit();
$this->capabilities = [
'mailpoetLogoInEmails' => new Capability('mailpoetLogoInEmails', Capability::TYPE_BOOLEAN, $this->isMailpoetLogoInEmailsRequired()),
'detailedAnalytics' => new Capability('detailedAnalytics', Capability::TYPE_BOOLEAN, !$this->isDetailedAnalyticsEnabled()),
'automationSteps' => new Capability('automationSteps', Capability::TYPE_NUMBER, $automationSteps > 0, $automationSteps),
'segmentFilters' => new Capability('segmentFilters', Capability::TYPE_NUMBER, $segmentFilters > 0, $segmentFilters),
];
}
/** @return array<string,Capability> */
public function getCapabilities(): array {
if ($this->capabilities === null) {
$this->initCapabilities();
}
return $this->capabilities ?? [];
}
public function getCapability(string $name): ?Capability {
return $this->getCapabilities()[$name] ?? null;
}
/**
* Returns true if there is no valid premium key or the product tier can be upgraded
*/
public function showUpgradePage(): bool {
$tier = $this->getTier();
if (!$this->subscribersFeature->hasValidPremiumKey() || (isset($tier) && $tier < self::MIN_TIER_NO_UPGRADE_PAGE)) {
return true;
}
return false;
}
/**
* Returns true if there is a valid key for a tier that can be upgraded
* @todo remove after the a/b test is finished and keep only one page
*/
public function showNewUpgradePage(): bool {
$tier = $this->getTier();
$isKeyValid = $this->subscribersFeature->hasValidPremiumKey() || $this->servicesChecker->isMailPoetAPIKeyValid(false);
if ($isKeyValid && isset($tier) && $tier < self::MIN_TIER_NO_UPGRADE_PAGE) {
return true;
}
return false;
}
}
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\License\Features\Data;
if (!defined('ABSPATH')) exit;
class Capability {
public const TYPE_BOOLEAN = 'boolean';
public const TYPE_NUMBER = 'number';
public string $name;
public string $type;
public ?int $value;
public bool $isRestricted;
public function __construct(
string $name,
string $type = self::TYPE_BOOLEAN,
bool $isRestricted = false,
?int $value = null
) {
$this->name = $name;
$this->type = $type;
$this->value = ($type === self::TYPE_NUMBER) ? $value : null;
$this->isRestricted = $isRestricted;
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,177 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\License\Features;
if (!defined('ABSPATH')) exit;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\WP\Functions as WPFunctions;
class Subscribers {
const SUBSCRIBERS_OLD_LIMIT = 2000;
const SUBSCRIBERS_NEW_LIMIT = 1000;
const NEW_LIMIT_DATE = '2019-11-00';
const MSS_KEY_STATE = 'mta.mailpoet_api_key_state.state';
const MSS_SUBSCRIBERS_LIMIT_SETTING_KEY = 'mta.mailpoet_api_key_state.data.site_active_subscriber_limit';
const MSS_SUPPORT_SETTING_KEY = 'mta.mailpoet_api_key_state.data.support_tier';
const PREMIUM_KEY_STATE = 'premium.premium_key_state.state';
const PREMIUM_SUBSCRIBERS_LIMIT_SETTING_KEY = 'premium.premium_key_state.data.site_active_subscriber_limit';
const MSS_EMAIL_VOLUME_LIMIT_SETTING_KEY = 'mta.mailpoet_api_key_state.data.email_volume_limit';
const MSS_EMAILS_SENT_SETTING_KEY = 'mta.mailpoet_api_key_state.data.emails_sent';
const PREMIUM_SUPPORT_SETTING_KEY = 'premium.premium_key_state.data.support_tier';
const SUBSCRIBERS_COUNT_CACHE_KEY = 'mailpoet_subscribers_count';
const SUBSCRIBERS_COUNT_CACHE_EXPIRATION_MINUTES = 60;
const SUBSCRIBERS_COUNT_CACHE_MIN_VALUE = 2000;
/** @var SettingsController */
private $settings;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var WPFunctions */
private $wp;
public function __construct(
SettingsController $settings,
SubscribersRepository $subscribersRepository,
WPFunctions $wp
) {
$this->settings = $settings;
$this->subscribersRepository = $subscribersRepository;
$this->wp = $wp;
}
/**
* Checks if the subscribers limit is reached
* @return bool True if subscribers limit reached or restriction set, false otherwise
*/
public function check(): bool {
$limit = $this->getSubscribersLimit();
$subscribersCount = $this->getSubscribersCount();
if ($limit && $subscribersCount > $limit) {
return true;
}
// We don't have data from MSS, or they might be outdated so we need to check accessibility restrictions
$mssStateData = $this->settings->get(Bridge::API_KEY_STATE_SETTING_NAME);
$restriction = $mssStateData['access_restriction'] ?? '';
return $restriction === Bridge::KEY_ACCESS_SUBSCRIBERS_LIMIT;
}
public function checkEmailVolumeLimitIsReached(): bool {
// We have data from MSS and we can determine based on the data
$emailVolumeLimit = $this->getEmailVolumeLimit();
$emailsSent = $this->getEmailsSent();
if ($emailVolumeLimit && $emailsSent > $emailVolumeLimit) {
return true;
}
// We don't have data from MSS, or they might be outdated so we need to check accessibility restrictions
$mssStateData = $this->settings->get(Bridge::API_KEY_STATE_SETTING_NAME);
$restriction = $mssStateData['access_restriction'] ?? '';
return $restriction === Bridge::KEY_ACCESS_EMAIL_VOLUME_LIMIT;
}
public function getSubscribersCount(): int {
$count = $this->wp->getTransient(self::SUBSCRIBERS_COUNT_CACHE_KEY);
if (is_numeric($count)) {
return (int)$count;
}
$count = $this->subscribersRepository->getTotalSubscribers();
// cache only when number of subscribers exceeds minimum value
if ($this->isSubscribersCountEnoughForCache($count)) {
$this->wp->setTransient(self::SUBSCRIBERS_COUNT_CACHE_KEY, $count, self::SUBSCRIBERS_COUNT_CACHE_EXPIRATION_MINUTES * 60);
}
return $count;
}
public function isSubscribersCountEnoughForCache(int $count = null): bool {
if (is_null($count) && func_num_args() === 0) {
$count = $this->getSubscribersCount();
}
return $count > self::SUBSCRIBERS_COUNT_CACHE_MIN_VALUE;
}
/**
* Returns true if key is valid or valid but underprivileged
* Do not use the method to check if key is valid for sending emails or premium
* This only means that the Bridge can authenticate the key.
* @return bool
*/
public function hasValidApiKey(): bool {
$mssState = $this->settings->get(self::MSS_KEY_STATE);
$premiumState = $this->settings->get(self::PREMIUM_KEY_STATE);
return $this->hasValidMssKey()
|| $this->hasValidPremiumKey()
|| $mssState === Bridge::KEY_VALID_UNDERPRIVILEGED
|| $premiumState === Bridge::KEY_VALID_UNDERPRIVILEGED;
}
public function getSubscribersLimit() {
if (!$this->hasValidApiKey()) {
return $this->getFreeSubscribersLimit();
}
$mssState = $this->settings->get(self::MSS_KEY_STATE);
if (($this->hasValidMssKey() || $mssState === Bridge::KEY_VALID_UNDERPRIVILEGED) && $this->hasMssSubscribersLimit()) {
return $this->getMssSubscribersLimit();
}
$premiumState = $this->settings->get(self::PREMIUM_KEY_STATE);
if (($this->hasValidPremiumKey() || $premiumState === Bridge::KEY_VALID_UNDERPRIVILEGED) && $this->hasPremiumSubscribersLimit()) {
return $this->getPremiumSubscribersLimit();
}
return false;
}
public function getEmailVolumeLimit(): int {
return (int)$this->settings->get(self::MSS_EMAIL_VOLUME_LIMIT_SETTING_KEY);
}
public function getEmailsSent(): int {
return (int)$this->settings->get(self::MSS_EMAILS_SENT_SETTING_KEY);
}
public function hasValidMssKey() {
$state = $this->settings->get(self::MSS_KEY_STATE);
return $state === Bridge::KEY_VALID || $state === Bridge::KEY_EXPIRING;
}
private function hasMssSubscribersLimit() {
return !empty($this->settings->get(self::MSS_SUBSCRIBERS_LIMIT_SETTING_KEY));
}
private function getMssSubscribersLimit() {
return (int)$this->settings->get(self::MSS_SUBSCRIBERS_LIMIT_SETTING_KEY);
}
public function hasMssPremiumSupport() {
return $this->hasValidMssKey() && $this->settings->get(self::MSS_SUPPORT_SETTING_KEY) === 'premium';
}
public function hasValidPremiumKey() {
$state = $this->settings->get(self::PREMIUM_KEY_STATE);
return $state === Bridge::KEY_VALID || $state === Bridge::KEY_EXPIRING;
}
private function hasPremiumSubscribersLimit() {
return !empty($this->settings->get(self::PREMIUM_SUBSCRIBERS_LIMIT_SETTING_KEY));
}
private function getPremiumSubscribersLimit() {
return (int)$this->settings->get(self::PREMIUM_SUBSCRIBERS_LIMIT_SETTING_KEY);
}
public function hasPremiumSupport() {
return $this->hasValidPremiumKey() && $this->settings->get(self::PREMIUM_SUPPORT_SETTING_KEY) === 'premium';
}
private function getFreeSubscribersLimit() {
$installationTime = strtotime((string)$this->settings->get('installed_at'));
$oldUser = $installationTime < strtotime(self::NEW_LIMIT_DATE);
return $oldUser ? self::SUBSCRIBERS_OLD_LIMIT : self::SUBSCRIBERS_NEW_LIMIT;
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,23 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\License;
if (!defined('ABSPATH')) exit;
class License {
const FREE_PREMIUM_SUBSCRIBERS_LIMIT = 1000;
public static function getLicense($license = false) {
if (!$license) {
$license = defined('MAILPOET_PREMIUM_LICENSE') ?
MAILPOET_PREMIUM_LICENSE :
false;
}
return $license;
}
public function hasLicense(): bool {
return (bool)self::getLicense();
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,51 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Helpers;
class AfterMigrationNotice {
const OPTION_NAME = 'mailpoet_display_after_migration_notice';
/** @var SettingsController */
private $settings;
public function __construct() {
$this->settings = SettingsController::getInstance();
}
public function enable() {
$this->settings->set(self::OPTION_NAME, true);
}
public function disable() {
$this->settings->set(self::OPTION_NAME, false);
}
public function init($shouldDisplay) {
if ($shouldDisplay && $this->settings->get(self::OPTION_NAME, false)) {
return $this->display();
}
}
private function display() {
$message = Helpers::replaceLinkTags(
__('Congrats! Youre progressing well so far. Complete your upgrade thanks to this [link]checklist[/link].', 'mailpoet'),
'https://kb.mailpoet.com/article/199-checklist-after-migrating-to-mailpoet3',
[
'target' => '_blank',
]
);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
$dataNoticeName = self::OPTION_NAME;
\MailPoet\WP\Notice::displaySuccess($message, $extraClasses, $dataNoticeName);
return $message;
}
}
@@ -0,0 +1,72 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\ServicesChecker;
use MailPoet\Util\License\Features\Subscribers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice as WPNotice;
class BlackFridayNotice {
const OPTION_NAME = 'dismissed-black-friday-notice';
const DISMISS_NOTICE_TIMEOUT_SECONDS = 2592000; // 30 days
const DATE_FROM = '2024-11-27 15:00:00 UTC';
const DATE_TO = '2024-12-03 15:00:00 UTC';
const PARAM_REF = 'sale-bfcm-2024-plugin';
const PARAM_UTM_CAMPAIGN = 'sale_bfcm_2024';
/** @var ServicesChecker */
private $servicesChecker;
/** @var Subscribers */
private $subscribers;
public function __construct(
ServicesChecker $servicesChecker,
Subscribers $subscribers
) {
$this->servicesChecker = $servicesChecker;
$this->subscribers = $subscribers;
}
public function init($shouldDisplay) {
$shouldDisplay = $shouldDisplay
&& !$this->servicesChecker->isBundledSubscription()
&& (time() >= strtotime(self::DATE_FROM))
&& (time() <= strtotime(self::DATE_TO))
&& !get_transient(self::OPTION_NAME);
if ($shouldDisplay) {
$this->display();
}
}
private function display() {
$header = '<h3 class="mailpoet-h3">' . __('Save 40% on all MailPoet annual plans and upgrades', 'mailpoet') . '</h3>';
$body = '<h5 class="mailpoet-h5">' . __('For a limited time, save 40% when you switch to (or upgrade) an annual plan — no coupon needed. Offer ends at 3 pm UTC, December 3, 2024.', 'mailpoet') . '</h5>';
$link = "<p><a href='" . $this->getSaleUrl() . "' class='mailpoet-button button-primary' target='_blank'>"
// translators: a button on a sale banner
. __('Pick a plan and save big', 'mailpoet')
. '</a></p>';
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
WPNotice::displaySuccess($header . $body . $link, $extraClasses, self::OPTION_NAME, false);
}
public function disable() {
WPFunctions::get()->setTransient(self::OPTION_NAME, true, self::DISMISS_NOTICE_TIMEOUT_SECONDS);
}
private function getSaleUrl(): string {
$params = 'ref=' . self::PARAM_REF . '&utm_source=plugin&utm_medium=banner&utm_campaign=' . self::PARAM_UTM_CAMPAIGN;
$partialApiKey = $this->servicesChecker->generatePartialApiKey();
if ($partialApiKey) {
return 'https://account.mailpoet.com/orders/upgrade/' . $partialApiKey . '?' . $params;
}
return 'https://account.mailpoet.com/?s=' . $this->subscribers->getSubscribersCount() . '&' . $params;
}
}
@@ -0,0 +1,42 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class ChangedTrackingNotice {
const OPTION_NAME = 'mailpoet-changed-tracking-settings-notice';
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
public function init($shouldDisplay) {
if ($shouldDisplay && $this->wp->getTransient(self::OPTION_NAME)) {
return $this->display();
}
return null;
}
public function display() {
$text = __('Email open and click tracking is now enabled. You can change how MailPoet tracks your subscribers in [link]Settings[/link]', 'mailpoet');
$text = Helpers::replaceLinkTags($text, 'admin.php?page=mailpoet-settings#advanced');
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
return Notice::displayWarning($text, $extraClasses, self::OPTION_NAME);
}
public function disable() {
$this->wp->deleteTransient(self::OPTION_NAME);
}
}
@@ -0,0 +1,117 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Env;
use MailPoet\Doctrine\WPDB\Connection;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class DatabaseEngineNotice {
const OPTION_NAME = 'database-engine-notice';
const DISMISS_NOTICE_TIMEOUT_SECONDS = 15_552_000; // 6 months
const CACHE_TIMEOUT_SECONDS = 86_400; // 1 day
const MAX_TABLES_TO_DISPLAY = 2;
private WPFunctions $wp;
private EntityManager $entityManager;
public function __construct(
WPFunctions $wp,
EntityManager $entityManager
) {
$this->wp = $wp;
$this->entityManager = $entityManager;
}
public function init($shouldDisplay): ?Notice {
if (!$shouldDisplay || Connection::isSQLite() || $this->wp->getTransient(self::OPTION_NAME)) {
return null;
}
try {
$tablesWithIncorrectEngine = $this->checkTableEngines();
if ($tablesWithIncorrectEngine === []) {
return null;
}
return $this->display($tablesWithIncorrectEngine);
} catch (\Exception $e) {
return null;
}
}
/**
* Returns a list of table names that are not using the InnoDB engine.
*/
private function checkTableEngines(): array {
$cacheKey = self::OPTION_NAME . '-cache';
$cachedTables = $this->wp->getTransient($cacheKey);
if (is_array($cachedTables)) {
return $cachedTables;
}
$tables = $this->loadTablesWithIncorrectEngines();
$this->wp->setTransient($cacheKey, $tables, self::CACHE_TIMEOUT_SECONDS);
return $tables;
}
private function loadTablesWithIncorrectEngines(): array {
$data = $this->entityManager->getConnection()->executeQuery(
'SHOW TABLE STATUS WHERE Name LIKE :prefix',
[
'prefix' => Env::$dbPrefix . '_%',
]
)->fetchAllAssociative();
return array_map(
fn($row) => $row['Name'],
array_filter(
$data,
fn($row) => isset($row['Engine']) && is_string($row['Engine']) && (strtolower($row['Engine']) !== 'innodb')
)
);
}
private function display(array $tablesWithIncorrectEngine): Notice {
// translators: %s is the list of the table names
$errorString = __('Some of the MailPoet plugins tables are not using the InnoDB engine (%s). This may cause performance and compatibility issues. Please ensure all MailPoet tables are converted to use the InnoDB engine. For more information, check out [link]this guide[/link].', 'mailpoet');
$tables = $this->formatTableNames($tablesWithIncorrectEngine);
$errorString = sprintf($errorString, $tables);
$error = Helpers::replaceLinkTags($errorString, 'https://kb.mailpoet.com/article/200-solving-database-connection-issues#database-configuration', [
'target' => '_blank',
]);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
return Notice::displayWarning($error, $extraClasses, self::OPTION_NAME);
}
private function formatTableNames(array $tablesWithIncorrectEngine): string {
sort($tablesWithIncorrectEngine);
$tables = array_map(
fn($table) => "{$table}",
array_slice($tablesWithIncorrectEngine, 0, self::MAX_TABLES_TO_DISPLAY)
);
$remainingTablesCount = count($tablesWithIncorrectEngine) - count($tables);
if ($remainingTablesCount > 0) {
// translators: %d is the number of remaining tables, the whole string will be: "table1, table2 and 3 more"
$tables[] = sprintf(__('and %d more', 'mailpoet'), $remainingTablesCount);
}
return implode(', ', $tables);
}
public function disable() {
$this->wp->setTransient(self::OPTION_NAME, true, self::DISMISS_NOTICE_TIMEOUT_SECONDS);
}
}
@@ -0,0 +1,52 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
/**
* This can be removed after 2022-12-01
*/
class DeprecatedFilterNotice {
const DISMISS_NOTICE_TIMEOUT_SECONDS = 15552000; // 6 months
const OPTION_NAME = 'dismissed-deprecated-filter-notice';
const DEPRECATED_FILTER_NAME = 'mailpoet_mailer_smtp_transport_agent';
const NEW_FILTER_NAME = 'mailpoet_mailer_smtp_options';
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
public function init($shouldDisplay): ?Notice {
if ($shouldDisplay && !$this->wp->getTransient(self::OPTION_NAME) && $this->wp->hasFilter('mailpoet_mailer_smtp_transport_agent')) {
return $this->display();
}
return null;
}
public function display(): Notice {
$message = Helpers::replaceLinkTags(
__('The <i>mailpoet_mailer_smtp_transport_agent</i> filter no longer works. Please replace it with <i>mailpoet_mailer_smtp_options</i>. Read more in [link]documentation[/link].', 'mailpoet'),
'https://kb.mailpoet.com/article/193-tls-encryption-does-not-work',
['target' => '_blank']
);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
return Notice::displayWarning($message, $extraClasses, self::OPTION_NAME);
}
public function disable(): void {
$this->wp->setTransient(self::OPTION_NAME, true, self::DISMISS_NOTICE_TIMEOUT_SECONDS);
}
}
@@ -0,0 +1,239 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Shortcodes;
use MailPoet\Doctrine\WPDB\Connection;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Helpers;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class DisabledMailFunctionNotice {
const DISABLED_MAIL_FUNCTION_CHECK = 'disabled_mail_function_check';
const QUEUE_DISABLED_MAIL_FUNCTION_CHECK = 'queue_disabled_mail_function_check';
/** @var SettingsController */
private $settings;
/** @var WPFunctions */
private $wp;
/** @var SubscribersFeature */
private $subscribersFeature;
/** @var MailerFactory */
private $mailerFactory;
private $isInQueueForChecking = false;
public function __construct(
WPFunctions $wp,
SettingsController $settings,
SubscribersFeature $subscribersFeature,
MailerFactory $mailerFactory
) {
$this->settings = $settings;
$this->wp = $wp;
$this->subscribersFeature = $subscribersFeature;
$this->mailerFactory = $mailerFactory;
}
public function init($shouldDisplay): ?string {
$shouldDisplay = $shouldDisplay && !Connection::isSQLite() && $this->shouldCheckMisconfiguredFunction() && $this->checkRequirements();
if (!$shouldDisplay) {
return null;
}
return $this->display();
}
private function checkRequirements(): bool {
if ($this->isInQueueForChecking) {
$this->settings->set(self::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, false);
}
$sendingMethod = $this->settings->get('mta.method', SettingsController::DEFAULT_SENDING_METHOD);
$isPhpMailSendingMethod = $sendingMethod === Mailer::METHOD_PHPMAIL;
if (!$isPhpMailSendingMethod) {
return false; // fails requirements check
}
$functionName = 'mail';
$isMailFunctionDisabled = $this->isFunctionDisabled($functionName);
if ($isMailFunctionDisabled) {
$this->settings->set(DisabledMailFunctionNotice::DISABLED_MAIL_FUNCTION_CHECK, true);
return true;
}
$isMailFunctionProperlyConfigured = $this->testMailFunctionIsCorrectlyConfigured();
return !$isMailFunctionProperlyConfigured;
}
/*
* Check MisConfigured Function
*
* This method will cause this class to only display the notice if the settings option
*
* disabled_mail_function_check === true
* or
* queue_disabled_mail_function_check === true
* or
* Totally disabled when wp filter `mailpoet_display_disabled_mail_function_notice` === false
*
*/
public function shouldCheckMisconfiguredFunction(): bool {
$shouldCheck = $this->wp->applyFilters('mailpoet_display_disabled_mail_function_notice', true);
$this->isInQueueForChecking = $this->settings->get(self::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, false);
return $shouldCheck && (
$this->settings->get(self::DISABLED_MAIL_FUNCTION_CHECK, false) ||
$this->isInQueueForChecking
);
}
public function isFunctionDisabled(string $function): bool {
$result = function_exists($function) && is_callable($function, false);
return !$result;
}
private function display(): string {
$header = $this->getHeader();
$body = $this->getBody();
$button = $this->getConnectMailPoetButton();
$message = $header . $body . $button;
Notice::displayWarning($message, '', self::DISABLED_MAIL_FUNCTION_CHECK, false);
return $message;
}
private function getHeader(): string {
return '<h4>' . __('Get ready to send your first campaign.', 'mailpoet') . '</h4>';
}
private function getBody(): string {
$bodyText = __('Connect your website with MailPoet, and start sending for free. Reach inboxes, not spam boxes. [link]Why am I seeing this?[/link]', 'mailpoet');
$bodyWithReplacedLink = Helpers::replaceLinkTags($bodyText, 'https://kb.mailpoet.com/article/396-disabled-mail-function', [
'target' => '_blank',
]);
return '<p>' . $bodyWithReplacedLink . '</p>';
}
private function getConnectMailPoetButton(): string {
$subscribersCount = $this->subscribersFeature->getSubscribersCount();
$buttonLink = "https://account.mailpoet.com/?s={$subscribersCount}&utm_source=mailpoet&utm_medium=plugin&utm_campaign=disabled_mail_function";
$link = $this->wp->escAttr($buttonLink);
return '<p><a target="_blank" href="' . $link . '" class="button button-primary">' . __('Connect MailPoet', 'mailpoet') . '</a></p>';
}
/*
* Test Mail Function Is Correctly Configured
*
* This is a workaround for detecting the user PHP mail() function is Correctly Configured and not disabled by the host
*/
private function testMailFunctionIsCorrectlyConfigured(): bool {
if ($this->settings->get(DisabledMailFunctionNotice::DISABLED_MAIL_FUNCTION_CHECK, false)) {
return false; // skip sending mail again
}
$replyToAddress = $this->settings->get('reply_to.address');
$senderAddress = $this->settings->get('sender.address');
$mailBody = "Hi there! \n
Your website ([site:homepage_link]) sent you this email to confirm that it can send emails.
If you're reading this email, then it works! You can now continue sending marketing emails with MailPoet! \n
MailPoet on [site:homepage_link]";
$body = Shortcodes::process($mailBody, null, null, null, null);
$sendTestMailData = [
'mailer' => $this->settings->get('mta'),
'newsletter' => [
'subject' => 'MailPoet can deliver your marketing emails!',
'body' => [
'html' => nl2br($body),
'text' => $body,
],
],
'subscriber' => empty($replyToAddress) ? $senderAddress : $replyToAddress,
];
$sendMailResult = $this->sendTestMail($sendTestMailData);
if (!$sendMailResult) {
// Error with PHP mail() function
// keep displaying notice
$this->settings->set(DisabledMailFunctionNotice::DISABLED_MAIL_FUNCTION_CHECK, true);
}
return $sendMailResult;
}
/*
* Send Test Mail
* used to check for valid PHP mail()
*
* returns true if valid and okay
* else returns false if invalid.
*
* We determine the mail function is invalid by checking against the Exception error thrown by PHPMailer
* error message: Could not instantiate mail function.
*
* if the error is not equal to error message, we consider it okay.
*/
public function sendTestMail($data = []): bool {
try {
$mailer = $this->mailerFactory->buildMailer(
$data['mailer'] ?? null,
$data['sender'] ?? null,
$data['reply_to'] ?? null
);
// report this as 'sending_test' in metadata since this endpoint is only used to test sending methods for now
$extraParams = [
'meta' => [
'email_type' => 'sending_test',
'subscriber_status' => 'unknown',
'subscriber_source' => 'administrator',
],
];
$result = $mailer->send($data['newsletter'], $data['subscriber'], $extraParams);
if ($result['response'] === false) {
$errorMessage = $result['error']->getMessage();
return !$this->checkForErrorMessage($errorMessage);
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
return !$this->checkForErrorMessage($errorMessage);
}
return true;
}
private function checkForErrorMessage($errorMessage): bool {
$phpmailerError = 'Could not instantiate mail function';
$substringIndex = stripos($errorMessage, $phpmailerError);
return $substringIndex !== false;
}
}
@@ -0,0 +1,79 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\CronTrigger;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class DisabledWPCronNotice {
const DISMISS_NOTICE_TIMEOUT_SECONDS = YEAR_IN_SECONDS;
const OPTION_NAME = 'dismissed-wp-cron-disabled-notice';
/** @var WPFunctions */
private $wp;
/** @var CronHelper */
private $cronHelper;
/** @var SettingsController */
private $settings;
public function __construct(
WPFunctions $wp,
CronHelper $cronHelper,
SettingsController $settings
) {
$this->wp = $wp;
$this->cronHelper = $cronHelper;
$this->settings = $settings;
}
public function init($shouldDisplay) {
if (!$shouldDisplay) {
return null;
}
$isDismissed = $this->wp->getTransient(self::OPTION_NAME);
$currentMethod = $this->settings->get(CronTrigger::SETTING_CURRENT_METHOD);
$isWPCronMethodActive = $currentMethod === CronTrigger::METHOD_ACTION_SCHEDULER;
$isCronFunctional = $this->isCronFunctional();
if (!$isDismissed && $isWPCronMethodActive && $this->isWPCronDisabled() && !$isCronFunctional) {
return $this->display();
}
}
public function isWPCronDisabled() {
return defined('DISABLE_WP_CRON') && DISABLE_WP_CRON;
}
public function isCronFunctional(): bool {
// If a cron run was started/completed less than an hour ago, we consider it functional.
$lastRunThreshold = time() - HOUR_IN_SECONDS;
return ($this->cronHelper->getDaemon()['run_started_at'] ?? 0) > $lastRunThreshold
|| ($this->cronHelper->getDaemon()['run_completed_at'] ?? 0) > $lastRunThreshold;
}
public function display() {
$errorString = __('WordPress built-in cron is disabled with the DISABLE_WP_CRON constant on your website, this prevents MailPoet sending from working. Please enable WordPress built-in cron or choose a different cron method in MailPoet Settings.', 'mailpoet');
$buttonString = __('[link]Go to Settings[/link]', 'mailpoet');
$error = $errorString . '<br><br>' . Helpers::replaceLinkTags($buttonString, 'admin.php?page=mailpoet-settings#advanced', [
'class' => 'button-primary',
]);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
return Notice::displayError($error, $extraClasses, self::OPTION_NAME, true, false);
}
public function disable() {
$this->wp->setTransient(self::OPTION_NAME, true, self::DISMISS_NOTICE_TIMEOUT_SECONDS);
}
}
@@ -0,0 +1,47 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class EmailWithInvalidSegmentNotice {
const OPTION_NAME = SendingQueue::EMAIL_WITH_INVALID_SEGMENT_OPTION;
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
public function init($shouldDisplay) {
if (!$shouldDisplay || !$this->wp->getTransient(self::OPTION_NAME)) {
return;
}
return $this->display($this->wp->getTransient(self::OPTION_NAME));
}
public function disable() {
$this->wp->deleteTransient(self::OPTION_NAME);
}
private function display($newsletterSubject) {
$notice = sprintf(
// translators: %s is the subject of the newsletter.
__('You are sending “%s“ to the deleted list. To continue sending, please restore the list. Alternatively, delete the newsletter if you no longer want to keep sending it.', 'mailpoet'),
$this->wp->escHtml($newsletterSubject)
);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
Notice::displayError($notice, $extraClasses, self::OPTION_NAME, true);
return $notice;
}
}
@@ -0,0 +1,93 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Captcha\CaptchaConstants;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class HeadersAlreadySentNotice {
const DISMISS_NOTICE_TIMEOUT_SECONDS = YEAR_IN_SECONDS;
const OPTION_NAME = 'dismissed-headers-already-sent-notice';
/** @var SettingsController */
private $settings;
/** @var TrackingConfig */
private $trackingConfig;
/** @var WPFunctions */
private $wp;
public function __construct(
SettingsController $settings,
TrackingConfig $trackingConfig,
WPFunctions $wp
) {
$this->settings = $settings;
$this->trackingConfig = $trackingConfig;
$this->wp = $wp;
}
public function init($shouldDisplay) {
if (!$shouldDisplay) {
return null;
}
$captchaEnabled = $this->settings->get('captcha.type') === CaptchaConstants::TYPE_BUILTIN;
$trackingEnabled = $this->trackingConfig->isEmailTrackingEnabled();
if ($this->areHeadersAlreadySent()) {
return $this->display($captchaEnabled, $trackingEnabled);
}
}
public function areHeadersAlreadySent() {
return !get_transient(self::OPTION_NAME)
&& ($this->headersSent() || $this->isWhitespaceInBuffer());
}
protected function headersSent() {
return headers_sent();
}
public function isWhitespaceInBuffer() {
$content = ob_get_contents();
if (!$content) {
return false;
}
return preg_match('/^\s+$/', $content);
}
public function display($captchaEnabled, $trackingEnabled) {
if (!$captchaEnabled && !$trackingEnabled) {
return null;
}
$errorString = __('It looks like there\'s an issue with some of the PHP files on your website which is preventing MailPoet from functioning correctly. If not resolved, you may experience:', 'mailpoet');
$errorStringTracking = __('Inaccurate tracking of email opens and clicks', 'mailpoet');
$errorStringCaptcha = __('CAPTCHA not rendering correctly', 'mailpoet');
$errorString = $errorString . '<br>'
. ($trackingEnabled ? ('<br> - ' . $errorStringTracking) : '')
. ($captchaEnabled ? ('<br> - ' . $errorStringCaptcha) : '');
$howToResolveString = __('[link]Learn how to fix this issue and restore functionality[/link]', 'mailpoet');
$error = $errorString . '<br><br>' . Helpers::replaceLinkTags($howToResolveString, 'https://kb.mailpoet.com/article/325-the-captcha-image-doesnt-show-up', [
'target' => '_blank',
'class' => 'button-primary',
]);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
return Notice::displayError($error, $extraClasses, self::OPTION_NAME, true, false);
}
public function disable() {
$this->wp->setTransient(self::OPTION_NAME, true, self::DISMISS_NOTICE_TIMEOUT_SECONDS);
}
}
@@ -0,0 +1,79 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class InactiveSubscribersNotice {
const OPTION_NAME = 'inactive-subscribers-notice';
const MIN_INACTIVE_SUBSCRIBERS_COUNT = 50;
/** @var SettingsController */
private $settings;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var WPFunctions */
private $wp;
public function __construct(
SettingsController $settings,
SubscribersRepository $subscribersRepository,
WPFunctions $wp
) {
$this->settings = $settings;
$this->wp = $wp;
$this->subscribersRepository = $subscribersRepository;
}
public function init($shouldDisplay) {
if (!$shouldDisplay || !$this->settings->get(self::OPTION_NAME, true)) {
return;
}
// don't display notice if user has changed the default inactive time range
$inactiveDays = (int)$this->settings->get('deactivate_subscriber_after_inactive_days');
if ($inactiveDays !== SettingsController::DEFAULT_DEACTIVATE_SUBSCRIBER_AFTER_INACTIVE_DAYS) {
return;
}
$inactiveSubscribersCount = $this->subscribersRepository->countBy(['deletedAt' => null, 'status' => SubscriberEntity::STATUS_INACTIVE]);
if ($inactiveSubscribersCount < self::MIN_INACTIVE_SUBSCRIBERS_COUNT) {
return;
}
return $this->display($inactiveSubscribersCount);
}
public function disable() {
$this->settings->set(self::OPTION_NAME, false);
}
private function display($inactiveSubscribersCount) {
$goToSettingsString = __('Go to the Advanced Settings', 'mailpoet');
$notice = sprintf(
// translators: %d is the number of inactive subscribers.
__('Good news! MailPoet wont send emails to your %s inactive subscribers. This is a standard practice to maintain good deliverability and open rates. But if you want to disable it, you can do so in settings. [link]Read more.[/link]', 'mailpoet'),
$this->wp->numberFormatI18n($inactiveSubscribersCount)
);
$notice = Helpers::replaceLinkTags($notice, 'https://kb.mailpoet.com/article/264-inactive-subscribers', [
'target' => '_blank',
]);
$notice = "<p>$notice</p>";
$notice .= '<p><a href="admin.php?page=mailpoet-settings#advanced" class="button button-primary">' . $goToSettingsString . '</a></p>';
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
Notice::displaySuccess($notice, $extraClasses, self::OPTION_NAME, false);
return $notice;
}
}
@@ -0,0 +1,43 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class PHPVersionWarnings {
const DISMISS_NOTICE_TIMEOUT_SECONDS = 2592000; // 30 days
const OPTION_NAME = 'dismissed-php-version-outdated-notice';
public function init($phpVersion, $shouldDisplay) {
if ($shouldDisplay && $this->isOutdatedPHPVersion($phpVersion)) {
return $this->display($phpVersion);
}
}
public function isOutdatedPHPVersion($phpVersion) {
return version_compare($phpVersion, '8.0', '<') && !get_transient(self::OPTION_NAME);
}
public function display($phpVersion) {
// translators: %s is the PHP version
$errorString = __('Your website is running an outdated version of PHP (%1$s), on which MailPoet might stop working in the future. We recommend upgrading to %2$s or greater. Read our [link]simple PHP upgrade guide.[/link]', 'mailpoet');
$errorString = sprintf($errorString, $phpVersion, '8.1');
$error = Helpers::replaceLinkTags($errorString, 'https://kb.mailpoet.com/article/251-upgrading-the-websites-php-version', [
'target' => '_blank',
]);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
return Notice::displayWarning($error, $extraClasses, self::OPTION_NAME);
}
public function disable() {
WPFunctions::get()->setTransient(self::OPTION_NAME, true, self::DISMISS_NOTICE_TIMEOUT_SECONDS);
}
}
@@ -0,0 +1,98 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Mailer;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Helpers;
use MailPoet\WP\Notice as WPNotice;
class PendingApprovalNotice {
const OPTION_NAME = 'mailpoet-pending-approval-notice';
/** @var SettingsController */
private $settings;
public function __construct(
SettingsController $settings
) {
$this->settings = $settings;
}
public function init($shouldDisplay): ?string {
// We should display the notice if the user is using MSS and the subscription is not approved
if (
$shouldDisplay
&& $this->settings->get('mta.method') === Mailer::METHOD_MAILPOET
&& $this->settings->get('mta.mailpoet_api_key_state')
&& $this->settings->get('mta.mailpoet_api_key_state.state', null) === Bridge::KEY_VALID
&& !$this->settings->get('mta.mailpoet_api_key_state.data.is_approved', false)
) {
return $this->display();
}
return null;
}
public function getPendingApprovalTitle(): string {
$message = __("MailPoet is [link]reviewing your subscription[/link].", 'mailpoet');
return Helpers::replaceLinkTags(
$message,
'https://kb.mailpoet.com/article/379-our-approval-process',
[
'target' => '_blank',
'rel' => 'noreferrer',
],
'link'
);
}
public function getPendingApprovalBody(): string {
// translators: %s is the email subject, which will always be in English
$message = sprintf(__("You can use all MailPoet features and send [link1]email previews[/link1] to your [link2]authorized email addresses[/link2], but sending to your email list contacts is temporarily paused until we review your subscription. If you don't hear from us within 48 hours, please check the inbox and spam folders of your MailPoet account email for follow-up emails with the subject \"%s\" and reply, or [link3]contact us[/link3].", 'mailpoet'), 'Your MailPoet Subscription Review');
$message = Helpers::replaceLinkTags(
$message,
'https://kb.mailpoet.com/article/290-check-your-newsletter-before-sending-it',
[
'target' => '_blank',
'rel' => 'noreferrer',
],
'link1'
);
$message = Helpers::replaceLinkTags(
$message,
'https://kb.mailpoet.com/article/266-how-to-add-an-authorized-email-address-as-the-from-address#how-to-authorize-an-email-address',
[
'target' => '_blank',
'rel' => 'noreferrer',
],
'link2'
);
$message = Helpers::replaceLinkTags(
$message,
'https://www.mailpoet.com/support/',
[
'target' => '_blank',
'rel' => 'noreferrer',
],
'link3'
);
return $message;
}
public function getPendingApprovalMessage(): string {
return sprintf('%s %s', $this->getPendingApprovalTitle(), $this->getPendingApprovalBody());
}
private function display(): string {
$message = $this->getPendingApprovalMessage();
WPNotice::displayWarning($message, '', self::OPTION_NAME);
return $message;
}
}
@@ -0,0 +1,225 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Menu;
use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class PermanentNotices {
/** @var WPFunctions */
private $wp;
/** @var PHPVersionWarnings */
private $phpVersionWarnings;
/** @var AfterMigrationNotice */
private $afterMigrationNotice;
/** @var UnauthorizedEmailNotice */
private $unauthorizedEmailsNotice;
/** @var UnauthorizedEmailInNewslettersNotice */
private $unauthorizedEmailsInNewslettersNotice;
/** @var InactiveSubscribersNotice */
private $inactiveSubscribersNotice;
/** @var BlackFridayNotice */
private $blackFridayNotice;
/** @var HeadersAlreadySentNotice */
private $headersAlreadySentNotice;
/** @var EmailWithInvalidSegmentNotice */
private $emailWithInvalidListNotice;
/** @var ChangedTrackingNotice */
private $changedTrackingNotice;
/** @var DeprecatedFilterNotice */
private $deprecatedFilterNotice;
/** @var DisabledMailFunctionNotice */
private $disabledMailFunctionNotice;
/** @var DisabledWPCronNotice */
private $disabledWPCronNotice;
/** @var PendingApprovalNotice */
private $pendingApprovalNotice;
/** @var WooCommerceVersionWarning */
private $woocommerceVersionWarning;
/** @var PremiumFeaturesAvailableNotice */
private $premiumFeaturesAvailableNotice;
/** @var SenderDomainAuthenticationNotices */
private $senderDomainAuthenticationNotices;
/** @var WordPressPlaygroundNotice */
private $wordPressPlaygroundNotice;
/** @var DatabaseEngineNotice */
private $databaseEngineNotice;
public function __construct(
WPFunctions $wp,
CronHelper $cronHelper,
EntityManager $entityManager,
TrackingConfig $trackingConfig,
SubscribersRepository $subscribersRepository,
SettingsController $settings,
SubscribersFeature $subscribersFeature,
ServicesChecker $serviceChecker,
MailerFactory $mailerFactory,
SenderDomainAuthenticationNotices $senderDomainAuthenticationNotices
) {
$this->wp = $wp;
$this->phpVersionWarnings = new PHPVersionWarnings();
$this->afterMigrationNotice = new AfterMigrationNotice();
$this->unauthorizedEmailsNotice = new UnauthorizedEmailNotice($wp, $settings);
$this->unauthorizedEmailsInNewslettersNotice = new UnauthorizedEmailInNewslettersNotice($settings, $wp);
$this->inactiveSubscribersNotice = new InactiveSubscribersNotice($settings, $subscribersRepository, $wp);
$this->blackFridayNotice = new BlackFridayNotice($serviceChecker, $subscribersFeature);
$this->headersAlreadySentNotice = new HeadersAlreadySentNotice($settings, $trackingConfig, $wp);
$this->emailWithInvalidListNotice = new EmailWithInvalidSegmentNotice($wp);
$this->changedTrackingNotice = new ChangedTrackingNotice($wp);
$this->deprecatedFilterNotice = new DeprecatedFilterNotice($wp);
$this->disabledMailFunctionNotice = new DisabledMailFunctionNotice($wp, $settings, $subscribersFeature, $mailerFactory);
$this->disabledWPCronNotice = new DisabledWPCronNotice($wp, $cronHelper, $settings);
$this->pendingApprovalNotice = new PendingApprovalNotice($settings);
$this->woocommerceVersionWarning = new WooCommerceVersionWarning($wp);
$this->premiumFeaturesAvailableNotice = new PremiumFeaturesAvailableNotice($subscribersFeature, $serviceChecker, $wp);
$this->databaseEngineNotice = new DatabaseEngineNotice($wp, $entityManager);
$this->wordPressPlaygroundNotice = new WordPressPlaygroundNotice();
$this->senderDomainAuthenticationNotices = $senderDomainAuthenticationNotices;
}
public function init() {
$excludeSetupWizard = [
'mailpoet-welcome-wizard',
'mailpoet-woocommerce-setup',
'mailpoet-landingpage',
];
$this->wp->addAction('wp_ajax_dismissed_notice_handler', [
$this,
'ajaxDismissNoticeHandler',
]);
$this->phpVersionWarnings->init(
phpversion(),
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->afterMigrationNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->unauthorizedEmailsNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->unauthorizedEmailsInNewslettersNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->inactiveSubscribersNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->blackFridayNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->headersAlreadySentNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->emailWithInvalidListNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->changedTrackingNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->deprecatedFilterNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->disabledMailFunctionNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->disabledWPCronNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->pendingApprovalNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->woocommerceVersionWarning->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->premiumFeaturesAvailableNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->databaseEngineNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->wordPressPlaygroundNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$excludeDomainAuthenticationNotices = [
'mailpoet-settings',
'mailpoet-newsletter-editor',
...$excludeSetupWizard,
];
$this->senderDomainAuthenticationNotices->init(
Menu::isOnMailPoetAdminPage($excludeDomainAuthenticationNotices)
);
}
public function ajaxDismissNoticeHandler() {
if (!isset($_POST['type'])) return;
switch ($_POST['type']) {
case (PHPVersionWarnings::OPTION_NAME):
$this->phpVersionWarnings->disable();
break;
case (AfterMigrationNotice::OPTION_NAME):
$this->afterMigrationNotice->disable();
break;
case (BlackFridayNotice::OPTION_NAME):
$this->blackFridayNotice->disable();
break;
case (HeadersAlreadySentNotice::OPTION_NAME):
$this->headersAlreadySentNotice->disable();
break;
case (InactiveSubscribersNotice::OPTION_NAME):
$this->inactiveSubscribersNotice->disable();
break;
case (EmailWithInvalidSegmentNotice::OPTION_NAME):
$this->emailWithInvalidListNotice->disable();
break;
case (ChangedTrackingNotice::OPTION_NAME):
$this->changedTrackingNotice->disable();
break;
case (DisabledWPCronNotice::OPTION_NAME):
$this->disabledWPCronNotice->disable();
break;
case (DeprecatedFilterNotice::OPTION_NAME):
$this->deprecatedFilterNotice->disable();
break;
case (WooCommerceVersionWarning::OPTION_NAME):
$this->woocommerceVersionWarning->disable();
break;
case (DatabaseEngineNotice::OPTION_NAME):
$this->databaseEngineNotice->disable();
break;
case (PremiumFeaturesAvailableNotice::OPTION_NAME):
$this->premiumFeaturesAvailableNotice->disable();
break;
}
}
}
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Installer;
use MailPoet\Config\ServicesChecker;
use MailPoet\Util\Helpers;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class PremiumFeaturesAvailableNotice {
/** @var SubscribersFeature */
private $subscribersFeature;
/** @var ServicesChecker */
private $servicesChecker;
/** @var Installer */
private $premiumInstaller;
/** @var WPFunctions */
private $wp;
const DISMISS_NOTICE_TIMEOUT_SECONDS = 2592000; // 30 days
const OPTION_NAME = 'dismissed-premium-features-available-notice';
public function __construct(
SubscribersFeature $subscribersFeature,
ServicesChecker $servicesChecker,
WPFunctions $wp
) {
$this->subscribersFeature = $subscribersFeature;
$this->servicesChecker = $servicesChecker;
$this->premiumInstaller = new Installer(Installer::PREMIUM_PLUGIN_PATH);
$this->wp = $wp;
}
public function init($shouldDisplay): ?Notice {
if (
$shouldDisplay
&& !$this->wp->getTransient(self::OPTION_NAME)
&& $this->subscribersFeature->hasValidPremiumKey()
&& (!Installer::isPluginInstalled(Installer::PREMIUM_PLUGIN_SLUG) || !$this->servicesChecker->isPremiumPluginActive())
) {
return $this->display();
}
return null;
}
public function display(): Notice {
$noticeString = __('Your current MailPoet plan includes advanced features, but they require the MailPoet Premium plugin to be installed and activated.', 'mailpoet');
// We reuse already existing translations from premium_messages.tsx
if (!Installer::isPluginInstalled(Installer::PREMIUM_PLUGIN_SLUG)) {
$noticeString .= ' [link]' . __('Download MailPoet Premium plugin', 'mailpoet') . '[/link]';
$link = $this->premiumInstaller->generatePluginDownloadUrl();
$attributes = ['target' => '_blank']; // Only download link should be opened in a new tab
} else {
$noticeString .= ' [link]' . __('Activate MailPoet Premium plugin', 'mailpoet') . '[/link]';
$link = $this->premiumInstaller->generatePluginActivationUrl(Installer::PREMIUM_PLUGIN_PATH);
$attributes = [];
}
$noticeString = Helpers::replaceLinkTags($noticeString, $link, $attributes);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
return Notice::displaySuccess($noticeString, $extraClasses, self::OPTION_NAME);
}
public function disable(): void {
WPFunctions::get()->setTransient(self::OPTION_NAME, true, self::DISMISS_NOTICE_TIMEOUT_SECONDS);
}
}
@@ -0,0 +1,188 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Services\AuthorizedSenderDomainController;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\FreeDomains;
use MailPoet\Util\Helpers;
use MailPoet\Util\License\Features\Subscribers;
use MailPoet\WP\Notice;
class SenderDomainAuthenticationNotices {
const FREE_MAIL_KB_URL = 'https://kb.mailpoet.com/article/259-your-from-address-cannot-be-yahoo-com-gmail-com-outlook-com';
const SPF_DKIM_DMARC_KB_URL = 'https://kb.mailpoet.com/article/295-spf-dkim-dmarc';
private SettingsController $settingsController;
private Subscribers $subscribersFeatures;
private FreeDomains $freeDomains;
private AuthorizedSenderDomainController $authorizedSenderDomainController;
private Bridge $bridge;
public function __construct(
SettingsController $settingsController,
Subscribers $subscribersFeatures,
FreeDomains $freeDomains,
AuthorizedSenderDomainController $authorizedEmailsController,
Bridge $bridge
) {
$this->settingsController = $settingsController;
$this->subscribersFeatures = $subscribersFeatures;
$this->freeDomains = $freeDomains;
$this->authorizedSenderDomainController = $authorizedEmailsController;
$this->bridge = $bridge;
}
public function getDefaultFromAddress(): string {
return $this->settingsController->get('sender.address', '');
}
public function getDefaultFromDomain(): string {
return Helpers::extractEmailDomain($this->getDefaultFromAddress());
}
public function isFreeMailUser(): bool {
return $this->freeDomains->isEmailOnFreeDomain($this->getDefaultFromDomain());
}
public function init($shouldDisplay): ?Notice {
if (
!$shouldDisplay
|| !$this->bridge->isMailpoetSendingServiceEnabled()
|| in_array($this->getDefaultFromDomain(), $this->authorizedSenderDomainController->getFullyVerifiedSenderDomains(true))
|| $this->authorizedSenderDomainController->isNewUser()
|| $this->isFreeMailUser() && $this->subscribersFeatures->getSubscribersCount() <= AuthorizedSenderDomainController::LOWER_LIMIT
) {
return null;
}
return $this->display();
}
public function display(): Notice {
$contactCount = $this->subscribersFeatures->getSubscribersCount();
$isFreeMailUser = $this->isFreeMailUser();
$noticeContent = $isFreeMailUser
? $this->getNoticeContentForFreeMailUsers($contactCount)
: $this->getNoticeContentForBrandedDomainUsers($this->isPartiallyVerified(), $contactCount);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
if ($this->isErrorStyle()) {
return Notice::displayError($noticeContent, $extraClasses, '', true, false);
}
return Notice::displayWarning($noticeContent, $extraClasses);
}
public function isErrorStyle(): bool {
if (
$this->subscribersFeatures->getSubscribersCount() < AuthorizedSenderDomainController::UPPER_LIMIT
|| $this->isPartiallyVerified()
) {
return false;
}
return true;
}
public function isPartiallyVerified(): bool {
return in_array($this->getDefaultFromDomain(), $this->authorizedSenderDomainController->getPartiallyVerifiedSenderDomains(true));
}
public function getNoticeContentForFreeMailUsers(int $contactCount): string {
if ($contactCount <= AuthorizedSenderDomainController::UPPER_LIMIT) {
// translators: %1$s is the domain of the user's default from address, %2$s is a rewritten version of their default from address, %3$s is HTML for an 'update sender' button, and %4$s is HTML for a Learn More button
return sprintf(
__("<strong>Update your sender email address to a branded domain to continue sending your campaigns.</strong>
<span>MailPoet can no longer send from email addresses on shared 3rd party domains like <strong>%1\$s</strong>. Please change your campaigns to send from an email address on your site's branded domain. Your existing scheduled and active emails will temporarily be sent from <strong>%2\$s</strong>.</span> <p>%3\$s &nbsp; %4\$s</p>", 'mailpoet'),
"@" . $this->getDefaultFromDomain(),
$this->authorizedSenderDomainController->getRewrittenEmailAddress($this->getDefaultFromAddress()),
$this->getUpdateSenderButton(),
$this->getLearnMoreAboutFreeMailButton()
);
}
// translators: %1$s is the domain of the user's default from address, %2$s is a rewritten version of their default from address, %3$s is HTML for an 'update sender' button, and %4$s is HTML for a Learn More button
return sprintf(
__("<strong>Your newsletters and post notifications have been paused. Update your sender email address to a branded domain to continue sending your campaigns.</strong>
<span>MailPoet can no longer send from email addresses on shared 3rd party domains like <strong>%1\$s</strong>. Please change your campaigns to send from an email address on your site's branded domain. Your marketing automations and transactional emails will temporarily be sent from <strong>%2\$s</strong>.</span> <p>%3\$s &nbsp; %4\$s</p>", 'mailpoet'),
"@" . $this->getDefaultFromDomain(),
$this->authorizedSenderDomainController->getRewrittenEmailAddress($this->getDefaultFromAddress()),
$this->getUpdateSenderButton(),
$this->getLearnMoreAboutFreeMailButton()
);
}
public function getNoticeContentForBrandedDomainUsers(bool $isPartiallyVerified, int $contactCount): string {
if ($isPartiallyVerified || $contactCount <= AuthorizedSenderDomainController::LOWER_LIMIT) {
// translators: %1$s is HTML for an 'authenticate domain' button, %2$s is HTML for a Learn More button
return sprintf(
__("<strong>Authenticate your sender domain to improve email delivery rates.</strong>
<span>Major mailbox providers require you to authenticate your sender domain to confirm you sent the emails, and may place unauthenticated emails in the “Spam” folder. Please authenticate your sender domain to ensure your marketing campaigns are compliant and will reach your contacts.</span><p>%1\$s &nbsp; %2\$s</p>", 'mailpoet'),
$this->getAuthenticateDomainButton(),
$this->getLearnMoreAboutSpfDkimDmarcButton()
);
}
if ($contactCount <= AuthorizedSenderDomainController::UPPER_LIMIT) {
// translators: %1$s is a rewritten version of the user's default from address, %2$s is HTML for an 'authenticate domain' button, %3$s is HTML for a Learn More button
return sprintf(
__("<strong>Authenticate your sender domain to send new emails.</strong>
<span>Major mailbox providers require you to authenticate your sender domain to confirm you sent the emails, and may place unauthenticated emails in the “Spam” folder. Please authenticate your sender domain to ensure your marketing campaigns are compliant and will reach your contacts. Your existing scheduled and active emails will temporarily be sent from <strong>%1\$s</strong>.</span> <p>%2\$s &nbsp; %3\$s</p>", 'mailpoet'),
$this->authorizedSenderDomainController->getRewrittenEmailAddress($this->getDefaultFromAddress()),
$this->getAuthenticateDomainButton(),
$this->getLearnMoreAboutSpfDkimDmarcButton()
);
}
// translators: %1$s is a rewritten version of the user's default from address, %2$s is HTML for an 'authenticate domain' button, %3$s is HTML for a Learn More button
return sprintf(
__("<strong>Your newsletters and post notifications have been paused. Authenticate your sender domain to continue sending.</strong>
<span>Major mailbox providers require you to authenticate your sender domain to confirm you sent the emails, and may place unauthenticated emails in the “Spam” folder. Please authenticate your sender domain to ensure your marketing campaigns are compliant and will reach your contacts. Your marketing automations and transactional emails will temporarily be sent from <strong>%1\$s</strong>.</span> <p>%2\$s &nbsp; %3\$s</p>", 'mailpoet'),
$this->authorizedSenderDomainController->getRewrittenEmailAddress($this->getDefaultFromAddress()),
$this->getAuthenticateDomainButton(),
$this->getLearnMoreAboutSpfDkimDmarcButton()
);
}
public function getUpdateSenderButton(): string {
$buttonClass = $this->subscribersFeatures->getSubscribersCount() > AuthorizedSenderDomainController::UPPER_LIMIT
? 'button-primary'
: 'button-secondary';
$button = sprintf('<a href="admin.php?page=mailpoet-settings" class="button %1$s">%2$s</a>', $buttonClass, __('Update sender email', 'mailpoet'));
return $button;
}
public function getLearnMoreAboutFreeMailButton(): string {
$button = '<a href="' . self::FREE_MAIL_KB_URL . '" rel="noopener noreferer" target="_blank" class="button button-link">' . __('Learn more', 'mailpoet') . '</a>';
return $button;
}
public function getLearnMoreAboutSpfDkimDmarcButton(): string {
$button = '<a href="' . self::SPF_DKIM_DMARC_KB_URL . '" rel="noopener noreferer" target="_blank" class="button button-link">' . __('Learn more', 'mailpoet') . '</a>';
return $button;
}
public function getAuthenticateDomainButton() {
$buttonClass = $this->isErrorStyle()
? 'button-primary'
: 'button-secondary';
$button = sprintf(
'<a href="#" class="button %s mailpoet-js-button-authorize-email-and-sender-domain" data-email="%s" data-type="domain">%s</a>',
$buttonClass,
esc_attr($this->getDefaultFromAddress()),
__('Authenticate domain', 'mailpoet')
);
return $button;
}
}
@@ -0,0 +1,73 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Menu;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\MailerLog;
use MailPoet\Newsletter\Renderer\EscapeHelper;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
class UnauthorizedEmailInNewslettersNotice {
const OPTION_NAME = 'unauthorized-email-in-newsletters-addresses-notice';
/** @var SettingsController */
private $settings;
/** @var WPFunctions */
private $wp;
public function __construct(
SettingsController $settings,
WPFunctions $wp
) {
$this->settings = $settings;
$this->wp = $wp;
}
public function init($shouldDisplay) {
$validationError = $this->settings->get(AuthorizedEmailsController::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING);
if ($shouldDisplay && isset($validationError['invalid_senders_in_newsletters'])) {
return $this->display($validationError);
}
}
public function display($validationError) {
$message = $this->getMessageText();
$message .= $this->getNewslettersLinks($validationError);
$message .= $this->getFixThisButton();
// Use Mailer log errors display system to display this notice
$mailerLog = MailerLog::setError(MailerLog::getMailerLog(), MailerError::OPERATION_AUTHORIZATION, $message);
MailerLog::updateMailerLog($mailerLog);
}
private function getMessageText() {
$message = __('<b>Your automatic emails have been paused</b> because some email addresses havent been authorized yet.', 'mailpoet');
return "<p>$message</p>";
}
private function getNewslettersLinks($validationError) {
$links = '';
foreach ($validationError['invalid_senders_in_newsletters'] as $error) {
// translators: %s is the newsletter subject.
$linkText = _x('Update the from address of %s', '%s will be replaced by a newsletter subject', 'mailpoet');
$linkText = str_replace('%s', EscapeHelper::escapeHtmlText($error['subject']), $linkText);
$linkUrl = $this->wp->adminUrl('admin.php?page=' . Menu::EMAILS_PAGE_SLUG . '#/send/' . $error['newsletter_id']);
$link = Helpers::replaceLinkTags("[link]{$linkText}[/link]", $linkUrl, ['target' => '_blank']);
$links .= "<p>$link</p>";
}
return $links;
}
private function getFixThisButton() {
$button = '<button class="button button-primary mailpoet-js-button-fix-this">' . __('Fix this!', 'mailpoet') . '</button>';
return "<p>$button</p>";
}
}
@@ -0,0 +1,86 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Newsletter\Renderer\EscapeHelper;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Settings\SettingsController;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class UnauthorizedEmailNotice {
const OPTION_NAME = 'unauthorized-email-addresses-notice';
/** @var SettingsController|null */
private $settings;
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp,
SettingsController $settings = null
) {
$this->settings = $settings;
$this->wp = $wp;
}
public function init($shouldDisplay) {
if (!$this->settings instanceof SettingsController) {
throw new \Exception('This method can only be called if SettingsController is provided');
}
$validationError = $this->settings->get(AuthorizedEmailsController::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING);
if ($shouldDisplay && isset($validationError['invalid_sender_address'])) {
return $this->display($validationError);
}
}
public function display($validationError) {
$message = $this->getMessage($validationError);
$extraClasses = 'mailpoet-js-error-unauthorized-emails-notice';
Notice::displayError($message, $extraClasses, self::OPTION_NAME, false, false);
}
public function getMessage($validationError) {
$message = $this->getMessageText($validationError);
$message .= sprintf(
'<p>%s &nbsp; %s &nbsp; %s</p>',
$this->getAuthorizeEmailButton($validationError),
$this->getDifferentEmailButton(),
$this->getResumeSendingButton($validationError)
);
return $message;
}
private function getMessageText($validationError) {
// translators: %s is the email address.
$text = _x(
'<b>Sending all of your emails has been paused</b> because your email address <b>%s</b> hasnt been authorized yet.',
'Email addresses have to be authorized to be used to send emails. %s will be replaced by an email address.',
'mailpoet'
);
$message = str_replace('%s', EscapeHelper::escapeHtmlText($validationError['invalid_sender_address']), $text);
return "<p>$message</p>";
}
private function getAuthorizeEmailButton($validationError) {
$email = $this->wp->escAttr($validationError['invalid_sender_address']);
$button = '<a target="_blank" href="https://account.mailpoet.com/authorization?email=' . $email . '" class="button button-primary mailpoet-js-button-authorize-email-and-sender-domain" data-type="email" data-email="' . $email . '">' . __('Authorize this email address', 'mailpoet') . '</a>';
return $button;
}
private function getDifferentEmailButton() {
$button = '<button class="button button-secondary mailpoet-js-button-fix-this">' . __('Use a different email address', 'mailpoet') . '</button>';
return $button;
}
private function getResumeSendingButton($validationError) {
$email = $this->wp->escAttr($validationError['invalid_sender_address']);
$button = '<button class="button button-secondary mailpoet-js-button-resume-sending" value="' . $email . '">' . __('Resume sending', 'mailpoet') . '</button>';
return $button;
}
}
@@ -0,0 +1,76 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Env;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
class WooCommerceVersionWarning {
const OPTION_NAME = 'mailpoet-dismissed-woo-version-outdated-notice';
const DISMISS_NOTICE_TIMEOUT_SECONDS = 2592000; // 30 days
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
public function init($shouldDisplay) {
if (!is_plugin_active('woocommerce/woocommerce.php')) {
return;
}
$woocommerceVersion = $this->getWoocommerceVersion();
$requiredWooCommerceVersion = $this->getRequiredWooCommerceVersion();
if ($shouldDisplay && $this->isOutdatedWooCommerceVersion($woocommerceVersion, $requiredWooCommerceVersion)) {
$this->display($requiredWooCommerceVersion);
}
}
public function isOutdatedWooCommerceVersion($woocommerceVersion, $requiredWooCommerceVersion): bool {
return version_compare($woocommerceVersion, $requiredWooCommerceVersion, '<') && !$this->wp->getTransient($this->getTransientKey());
}
private function display($requiredWooCommerceVersion) {
// translators: %s is the PHP version
$errorString = __('MailPoet plugin requires WooCommerce version %s or newer. Please update your WooCommerce plugin version, or read our [link]instructions[/link] for additional options on how to resolve this issue.', 'mailpoet');
$errorString = sprintf($errorString, $requiredWooCommerceVersion);
$error = Helpers::replaceLinkTags($errorString, 'https://kb.mailpoet.com/article/152-minimum-requirements-for-mailpoet-3#woocommerce-version', [
'target' => '_blank',
]);
$extraClasses = 'mailpoet-dismissible-notice is-dismissible';
Notice::displayWarning($error, $extraClasses, self::OPTION_NAME);
}
public function disable() {
$this->wp->setTransient($this->getTransientKey(), true, self::DISMISS_NOTICE_TIMEOUT_SECONDS);
}
private function getTransientKey() {
$woocommerceVersion = $this->getWoocommerceVersion();
return self::OPTION_NAME . '_' . $this->getRequiredWooCommerceVersion() . '_' . $woocommerceVersion;
}
private function getWoocommerceVersion(): string {
return $this->wp->getPluginData(WP_PLUGIN_DIR . '/woocommerce/woocommerce.php', false, false)['Version'];
}
private function getRequiredWooCommerceVersion(): string {
$pluginData = $this->wp->getFileData(
Env::$file,
[
'RequiredWCVersion' => 'WC requires at least',
]
);
return $pluginData['RequiredWCVersion'] ?? '100.0.0';
}
}
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\WPDB\Connection;
use MailPoet\WP\Notice;
class WordPressPlaygroundNotice {
const OPTION_NAME = 'wordpress-playground-notice';
public function init($shouldDisplay): ?Notice {
if (!$shouldDisplay || !Connection::isSQLite()) {
return null;
}
return $this->display();
}
private function display(): Notice {
return Notice::displayWarning(
sprintf(
'<p><strong>%s</strong></p><p>%s</p>',
__('MailPoet Preview', 'mailpoet'),
__('This is a preview of the MailPoet plugin. Please note that some functionality may be limited.', 'mailpoet')
),
null,
self::OPTION_NAME,
false
);
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,12 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
class Request {
public function isPost(): bool {
return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' || count($_POST) > 0;
}
}
@@ -0,0 +1,15 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
class SecondLevelDomainNames {
public function get($host) {
if (preg_match('/[^.]*\.[^.]{2,3}(?:\.[^.]{2,3})?$/', $host, $matches)) {
return $matches[0];
}
return $host;
}
}
@@ -0,0 +1,97 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use Exception;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Subscribers\SubscribersRepository;
class Security {
const HASH_LENGTH = 12;
const UNSUBSCRIBE_TOKEN_LENGTH = 15;
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
NewslettersRepository $newslettersRepository,
SubscribersRepository $subscribersRepository
) {
$this->newslettersRepository = $newslettersRepository;
$this->subscribersRepository = $subscribersRepository;
}
/**
* Generate random lowercase alphanumeric string.
* 1 lowercase alphanumeric character = 6 bits (because log2(36) = 5.17)
* So 3 bytes = 4 characters
* @param int $length Minimal lenght is 5
* @return string
*/
public static function generateRandomString($length = 5): string {
$length = max(5, (int)$length);
$string = base_convert(
bin2hex(
random_bytes( // phpcs:ignore
(int)ceil(3 * $length / 4)
)
),
16,
36
);
$result = substr($string, 0, $length);
if (strlen($result) === $length) return $result;
// in very rare occasions we generate a shorter string when random_bytes generates something starting with 0 let's try again
return self::generateRandomString($length);
}
/**
* @param int $length Maximal length is 32
* @return string
*/
public static function generateHash($length = null) {
$length = ($length) ? $length : self::HASH_LENGTH;
$authKey = self::generateRandomString(64);
if (defined('AUTH_KEY')) {
$authKey = AUTH_KEY;
}
return substr(
hash_hmac('sha512', self::generateRandomString(64), $authKey),
0,
$length
);
}
static public function generateUnsubscribeToken($model) {
do {
$token = self::generateRandomString(self::UNSUBSCRIBE_TOKEN_LENGTH);
$found = $model::whereEqual('unsubscribe_token', $token)->count();
} while ($found > 0);
return $token;
}
public function generateUnsubscribeTokenByEntity($entity): string {
$repository = null;
if ($entity instanceof NewsletterEntity) {
$repository = $this->newslettersRepository;
} elseif ($entity instanceof SubscriberEntity) {
$repository = $this->subscribersRepository;
} else {
throw new Exception('Unsupported Entity type');
}
do {
$token = self::generateRandomString(self::UNSUBSCRIBE_TOKEN_LENGTH);
$found = count($repository->findBy(['unsubscribeToken' => $token]));
} while ($found > 0);
return $token;
}
}
@@ -0,0 +1,79 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util;
if (!defined('ABSPATH')) exit;
use MailPoet\WP\Functions as WPFunctions;
class Url {
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
public function getCurrentUrl() {
$homeUrl = parse_url($this->wp->homeUrl());
$queryArgs = $this->wp->addQueryArg(null, null);
// Remove $this->wp->homeUrl() path from add_query_arg
if (
is_array($homeUrl)
&& isset($homeUrl['path'])
) {
$queryArgs = str_replace($homeUrl['path'], '', $queryArgs);
}
return $this->wp->homeUrl($queryArgs);
}
public function redirectTo($url = null) {
$this->wp->wpSafeRedirect($url);
exit();
}
public function redirectBack($params = []) {
// check mailpoet_redirect parameter
$referer = (isset($_POST['mailpoet_redirect'])
? sanitize_text_field(wp_unslash($_POST['mailpoet_redirect']))
: $this->wp->wpGetReferer()
);
// fallback: home_url
if (!$referer) {
$referer = $this->wp->homeUrl();
}
// append extra params to url
if (!empty($params)) {
$referer = $this->wp->addQueryArg($params, $referer);
}
$this->redirectTo($referer);
exit();
}
public function redirectWithReferer($url = null) {
$currentUrl = $this->getCurrentUrl();
$url = $this->wp->addQueryArg(
[
'mailpoet_redirect' => urlencode($currentUrl),
],
$url
);
if ($url !== $currentUrl) {
$this->redirectTo($url);
}
exit();
}
public function isUsingHttps(string $url): bool {
return $this->wp->wpParseUrl($url, PHP_URL_SCHEME) === 'https';
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,27 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\pQuery;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\pQuery\DomNode as pQueryDomNode;
class DomNode extends pQueryDomNode {
public $childClass = DomNode::class;
public $parserClass = Html5Parser::class;
public function getInnerText() {
return html_entity_decode($this->toString(true, true, 1), ENT_NOQUOTES, 'UTF-8');
}
public function getOuterText() {
return html_entity_decode($this->toString(), ENT_NOQUOTES, 'UTF-8');
}
public function query($query = '*') {
$select = $this->select($query);
$result = new pQuery((array)$select);
return $result;
}
}
@@ -0,0 +1,13 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\pQuery;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\pQuery\Html5Parser as pQueryHtml5Parser;
class Html5Parser extends pQueryHtml5Parser {
/** @var string|DomNode */
public $root = DomNode::class;
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,23 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Util\pQuery;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\pQuery\pQuery as pQuerypQuery;
// extend pQuery class to use UTF-8 encoding when getting elements' inner/outer text
// phpcs:ignore Squiz.Classes.ValidClassName
class pQuery extends pQuerypQuery {
public static function parseStr($html): DomNode {
$parser = new Html5Parser($html);
if (!$parser->root instanceof DomNode) {
// this condition shouldn't happen it is here only for PHPStan
throw new \Exception('Renderer is not configured correctly');
}
return $parser->root;
}
}