Compare commits

..

40 Commits
5.0.1 ... 5.0.2

Author SHA1 Message Date
b602edae52 Release 5.0.2 2024-08-26 16:33:59 +02:00
7ebcf324a6 Update code for the new cron-expression package
[MAILPOET-6167]
2024-08-26 14:52:05 +02:00
029b698b56 Replace cron-expression package with a fork
[MAILPOET-6167]
2024-08-26 14:52:05 +02:00
cbc2be2368 Remove the MYISAM tests
Now we have a check and warn users
InnoDB has been a default for 14 years

[MAILPOET-6184]
2024-08-26 10:20:45 +02:00
95476418fa Update used WooCommerce Subscriptions plugin in Circle CI
- latest version: 6.6.0
 - previous version: 6.5.0
2024-08-26 09:13:00 +02:00
db354f03c2 Update used Automate Woo plugin in Circle CI
- latest version: 6.0.32
 - previous version: 5.8.5
2024-08-26 09:13:00 +02:00
60c18e9cfa Update used WooCommerce plugin in Circle CI
- latest version: 9.2.2
 - previous version: 9.1.4
2024-08-26 09:13:00 +02:00
c75bcbd1ab Add Ukrainian community translations. Thanks Sasha
[MAILPOET-6198]
2024-08-26 08:55:28 +02:00
a91fd0abf1 Replace t() with __() in task scheduler translations
[MAILPOET-6200]
2024-08-25 22:42:20 +02:00
7068a7e6b1 Rename action scheduler cron option to prevent confusion for HEs
[MAILPOET-6200]
2024-08-25 22:42:20 +02:00
a94efb5563 Release 5.0.1 2024-08-23 16:32:40 +02:00
7756827b6e Remove unused parameters in MailPoet.date.toDate
The options were just set in init but there was no effect.
[MAILPOET-6197]
2024-08-23 15:36:00 +02:00
82c0b186d4 Do not apply offset when manipulating with date in the Date picker
We use DateTime component on the send page.
The DateTime component uses DateText for picking date.
We add site's offset before we pass the date to the DateTime.

The date-text component is nested in the DateTime and was also applying
offset on the date.

The second issue was we needed to convert the value to JS Date.
The MailPoet.Date.toDate expects the value to be in UTC, and we provided an already converted date.

So for offset -07:00
MailPoet.Date.toDate('2023-09-01');
ends up
Thu Aug 31 2023 17:00:00 GMT-0700 (Pacific Daylight Time) and calendar display's incorrect value.

This commit fixes it so that we don't touch offset in the DateText component.
[MAILPOET-6197]
2024-08-23 15:36:00 +02:00
f960b55acb Make sure the array contains the correct key
[MAILPOET-6184]
2024-08-22 14:33:54 +02:00
d137b55dd4 Catch errors while performing query
[MAILPOET-6184]
2024-08-22 14:33:54 +02:00
c039b220a7 Check the tables engine once per day
[MAILPOET-6184]
2024-08-22 14:33:54 +02:00
e9969b64ae Only display two table names
[MAILPOET-6184]
2024-08-22 14:33:54 +02:00
12cdba005c Add a warning if a table with incorrect engine detected
[MAILPOET-6184]
2024-08-22 14:33:54 +02:00
3c9cde6a45 Add an empty test
[MAILPOET-6184]
2024-08-22 14:33:54 +02:00
64c75fd0ca Create a new class for database notice
[MAILPOET-6184]
2024-08-22 14:33:54 +02:00
dd685f3284 Use __() for translations in new tsx file
[MAILPOET-6164]
2024-08-22 14:11:26 +02:00
b77cd11c02 Update tests
[MAILPOET-6164]
2024-08-22 14:11:26 +02:00
0f4bf52ca2 Hide machine opens in UI when opens are merged
[MAILPOET-6164]
2024-08-22 14:11:26 +02:00
8a506e7278 Show merged opens if respective setting is set
[MAILPOET-6164]
2024-08-22 14:11:26 +02:00
ee1043fce9 Add human and machine opens setting
[MAILPOET-6164]
2024-08-22 14:11:26 +02:00
c987733fba Make the form border consistent
In the renderer we only display the border if both
colour and size are set. Before this commit in the
editor we showed the border even if the colour
was transparent. This make it consistent and display
the border in the editor same way as in the renderer

[MAILPOET-6193]
2024-08-22 13:56:54 +02:00
912cd7965a Temporarily replace DROP DEFAULT with a CHANGE COLUMN for WP Playground
[MAILPOET-6185]
2024-08-22 13:28:09 +02:00
f584a673cb Temporarily skip queries incompatible with SQLite
[MAILPOET-6185]
2024-08-22 13:28:09 +02:00
4b69005900 Use subquery in DELETE for SQLite
[MAILPOET-6185]
2024-08-22 13:28:09 +02:00
279364cf86 Avoid using information_schema for WP Playground 2024-08-22 13:28:09 +02:00
53919dd71b Add helper for detecting SQLite database engine
[MAILPOET-6185]
2024-08-22 13:28:09 +02:00
ad46417ee8 Add error handling logic for SQLite
[MAILPOET-6185]
2024-08-22 13:28:09 +02:00
a492658d48 Fix incorrect PHP version in comments
[MAILPOET-6195]
2024-08-21 19:23:28 +02:00
74b9d3788f Improve test for rollback to also check the outcome on the MyISAM engine
[MAILPOET-6195]
2024-08-21 19:23:28 +02:00
6b3592c4be Change default ordering in Automation analytics with subscribers
[MAILPOET-6174]
2024-08-21 15:15:21 +02:00
6b5fb2e92d Make all error messages translatable
[MAILPOET-6174]
2024-08-21 15:15:21 +02:00
50a00789b8 Add setting error message when email is missing
[MAILPOET-6174]
2024-08-21 15:15:21 +02:00
ecf0e1d2db Update error message in SendEmailAction
[MAILPOET-6174]
2024-08-21 15:15:21 +02:00
0761998eba Use typed properties in SendEmailAction
[MAILPOET-6174]
2024-08-21 15:15:21 +02:00
54043e5364 Update error messages in automations
[MAILPOET-6174]
2024-08-21 15:15:21 +02:00
42 changed files with 791 additions and 213 deletions

View File

@ -191,10 +191,10 @@ jobs:
- run:
name: Download additional WP Plugins for tests
command: |
./do download:woo-commerce-zip 9.1.4
./do download:woo-commerce-subscriptions-zip 6.5.0
./do download:woo-commerce-zip 9.2.2
./do download:woo-commerce-subscriptions-zip 6.6.0
./do download:woo-commerce-memberships-zip 1.26.5
./do download:automate-woo-zip 6.0.31
./do download:automate-woo-zip 6.0.32
- run:
name: Dump tests ENV variables for acceptance tests
command: |
@ -1065,14 +1065,14 @@ workflows:
- acceptance_tests:
<<: *slack-fail-post-step
name: acceptance_oldest
woo_core_version: 9.0.2
woo_subscriptions_version: 6.4.1
woo_core_version: 9.1.4
woo_subscriptions_version: 6.5.0
woo_memberships_version: 1.25.2
automate_woo_version: 5.8.5
mysql_command: --max_allowed_packet=100M --default-storage-engine=MYISAM
mysql_command: --max_allowed_packet=100M
mysql_image: mysql:5.5
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 6.4 and install required WordPress version via CLI
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
wordpress_version: 6.5.5
requires:
- build
@ -1106,14 +1106,14 @@ workflows:
- integration_tests:
<<: *slack-fail-post-step
name: integration_oldest
woo_core_version: 9.0.2
woo_subscriptions_version: 6.4.1
woo_core_version: 9.1.4
woo_subscriptions_version: 6.5.0
woo_memberships_version: 1.25.2
automate_woo_version: 5.8.5
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 6.4 and install required WordPress version via CLI # We use image with PHP 6.4 and install required WordPress version via CLI
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI # We use image with PHP 7.4 and install required WordPress version via CLI
wordpress_version: 6.5.5
mysql_command: --max_allowed_packet=100M --default-storage-engine=MYISAM
mysql_command: --max_allowed_packet=100M
mysql_image: mysql:5.5
requires:
- build
@ -1169,26 +1169,26 @@ workflows:
- acceptance_tests:
<<: *slack-fail-post-step
name: acceptance_with_premium_oldest
woo_core_version: 9.0.2
woo_subscriptions_version: 6.4.1
woo_core_version: 9.1.4
woo_subscriptions_version: 6.5.0
woo_memberships_version: 1.25.2
automate_woo_version: 5.8.5
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 6.4 and install required WordPress version via CLI
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
wordpress_version: 6.5.5
requires:
- build_premium
- integration_tests:
<<: *slack-fail-post-step
name: integration_with_premium_oldest
woo_core_version: 9.0.2
woo_subscriptions_version: 6.4.1
woo_core_version: 9.1.4
woo_subscriptions_version: 6.5.0
woo_memberships_version: 1.25.2
automate_woo_version: 5.8.5
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 6.4 and install required WordPress version via CLI
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
wordpress_version: 6.5.5
mysql_command: --max_allowed_packet=100M --default-storage-engine=MYISAM
mysql_command: --max_allowed_packet=100M
mysql_image: mysql:5.5
requires:
- build_premium

View File

@ -1,5 +1,11 @@
== Changelog ==
= 5.0.2 - 2024-08-26 =
- Added: Ukrainian translations;
- Improved: error messages in automations;
- Changed: human and machine opens are merged by default, old behavior can be restored in settings.
= 5.0.1 - 2024-08-23 =
- Fixed: incorrect date in the scheduling calendar on the send page.

View File

@ -650,7 +650,7 @@ class RoboFile extends \Robo\Tasks {
'lib/',
'lib-3rd-party/',
'vendor/composer',
'vendor/mtdowling',
'vendor/dragonmantank',
'vendor-prefixed/',
'vendor-prefixed/soundasleep',
'mailpoet.php',
@ -666,7 +666,7 @@ class RoboFile extends \Robo\Tasks {
'vendor-prefixed/cerdic/css-tidy/COPYING',
'vendor-prefixed/cerdic/css-tidy/NEWS',
'vendor-prefixed/cerdic/css-tidy/testing',
'vendor/mtdowling/cron-expression/tests',
'vendor/dragonmantank/cron-expression/tests',
'vendor/phpmailer/phpmailer/test',
'vendor-prefixed/psr/log/Psr/Log/Test',
'vendor-prefixed/sabberworm/php-css-parser/tests',

View File

@ -38,7 +38,7 @@ const sections: Record<string, Section> = {
},
},
customQuery: {
order: 'asc',
order: 'desc',
order_by: 'updated_at',
limit: 25,
page: 1,

View File

@ -28,11 +28,6 @@ function FormStylingBackground({ children }) {
[previewSettings.formType],
);
let borderStyle;
if (borderSize && borderColor) {
borderStyle = 'solid';
}
let radius;
if (borderRadius) radius = Number(borderRadius);
let padding;
@ -51,9 +46,6 @@ function FormStylingBackground({ children }) {
fontFamily,
lineHeight: 1.2,
borderRadius: radius,
borderWidth: borderSize,
borderColor,
borderStyle,
textAlign,
padding,
width: formWidth.unit === 'pixel' ? formWidth.value : `${formWidth.value}%`,
@ -61,6 +53,12 @@ function FormStylingBackground({ children }) {
maxWidth: '100%',
};
if (borderSize && borderColor) {
style.borderWidth = borderSize;
style.borderColor = borderColor;
style.borderStyle = 'solid';
}
// Render virtual container for widgets and below pages/post forms with width in percent
if (
['others', 'below_posts'].includes(previewSettings.formType) &&

View File

@ -145,6 +145,9 @@ interface Window {
level: 'full' | 'partial' | 'basic';
cookieTrackingEnabled: boolean;
emailTrackingEnabled: boolean;
opens: 'merged' | 'separated';
opensMerged: boolean;
opensSeparated: boolean;
}>;
mailpoet_display_detailed_stats: boolean;
mailpoet_premium_plugin_installed: boolean;

View File

@ -255,7 +255,7 @@ function NewsletterGeneralStats({ newsletter, isWoocommerceActive }: Props) {
<div>{clicked}</div>
<div className="mailpoet-statistics-with-left-separator">
{opened}
{machineOpened}
{MailPoet.trackingConfig.opensSeparated && machineOpened}
</div>
{isWoocommerceActive && (
<div className="mailpoet-statistics-with-left-separator">

View File

@ -2,6 +2,7 @@ import { SaveButton } from 'settings/components';
import { TaskScheduler } from './task-scheduler';
import { Roles } from './roles';
import { EngagementTracking } from './engagement-tracking';
import { HumanAndMachineOpens } from './human-and-machine-opens';
import { Transactional } from './transactional';
import { InactiveSubscribers } from './inactive-subscribers';
import { ShareData } from './share-data';
@ -19,6 +20,7 @@ export function Advanced() {
<TaskScheduler />
<Roles />
<EngagementTracking />
<HumanAndMachineOpens />
<Transactional />
<RecalculateSubscriberScore />
<InactiveSubscribers />

View File

@ -0,0 +1,54 @@
import { ReactElement } from 'react';
import { Radio } from 'common/form/radio/radio';
import { useSetting } from 'settings/store/hooks';
import { Label, Inputs } from 'settings/components';
import { __ } from '@wordpress/i18n';
export function HumanAndMachineOpens(): ReactElement {
const [opens, setOpensMode] = useSetting('tracking', 'opens');
return (
<>
<Label
title={__('Human and machine opens', 'mailpoet')}
description={__(
'Choose how human and machine opens should be displayed.',
'mailpoet',
)}
htmlFor="opens_mode"
/>
<Inputs>
<div className="mailpoet-settings-inputs-row">
<Radio
id="opens-merged"
value="merged"
checked={opens === 'merged'}
onCheck={setOpensMode}
automationId="opens-merged-radio"
/>
<label htmlFor="opens-merged">
{__(
'Merged both are counted as total opens. Similar to other email marketing tools.',
'mailpoet',
)}
</label>
</div>
<div className="mailpoet-settings-inputs-row">
<Radio
id="opens-separated"
value="separated"
checked={opens === 'separated'}
onCheck={setOpensMode}
automationId="opens-separated-radio"
/>
<label htmlFor="opens-separated">
{__(
'Separated only human opens are counted as total opens. More accurate, but the numbers tend to be lower.',
'mailpoet',
)}
</label>
</div>
</Inputs>
</>
);
}

View File

@ -1,4 +1,4 @@
import { t } from 'common/functions';
import { __ } from '@wordpress/i18n';
import { Input } from 'common/form/input/input';
import { Radio } from 'common/form/radio/radio';
import { useSetting, useSelector } from 'settings/store/hooks';
@ -11,17 +11,20 @@ export function TaskScheduler() {
return (
<>
<Label
title={t('taskCron')}
title={__('Newsletter task scheduler (cron)', 'mailpoet')}
description={
<>
{t('taskCronDescription')}{' '}
{__('Select what will activate your newsletter queue.', 'mailpoet')}{' '}
<a
className="mailpoet-link"
href="https://kb.mailpoet.com/article/129-what-is-the-newsletter-task-scheduler"
rel="noopener noreferrer"
target="_blank"
>
{t('readMore')}
{
// translators: support article link label
__('Read more.', 'mailpoet')
}
</a>
</>
}
@ -37,7 +40,7 @@ export function TaskScheduler() {
automationId="action_scheduler_cron_radio"
/>
<label htmlFor="cron_trigger-method-action-scheduler">
{t('actionSchedulerCron')}
{__('Action Scheduler (recommended)', 'mailpoet')}
</label>
</div>
<div className="mailpoet-settings-inputs-row">
@ -49,7 +52,7 @@ export function TaskScheduler() {
automationId="wordress_cron_radio"
/>
<label htmlFor="cron_trigger-method-wordpress">
{t('websiteVisitors')}
{__('Visitors to your website', 'mailpoet')}
</label>
</div>
<div className="mailpoet-settings-inputs-row">
@ -60,12 +63,17 @@ export function TaskScheduler() {
onCheck={setMethod}
automationId="linux_cron_radio"
/>
<label htmlFor="cron_trigger-method-cron">{t('serverCron')}</label>
<label htmlFor="cron_trigger-method-cron">
{__('Server side cron (Linux cron)', 'mailpoet')}
</label>
</div>
{method === 'Linux Cron' && (
<div className="mailpoet-settings-inputs-row">
<div className="mailpoet-settings-inputs-row">
{t('addCommandToCrontab')}
{__(
'To use this option please add this command to your crontab:',
'mailpoet',
)}
</div>
<Input
dimension="small"
@ -74,7 +82,7 @@ export function TaskScheduler() {
value={`php ${paths.plugin}/mailpoet-cron.php ${paths.root}`}
/>
<div className="mailpoet-settings-inputs-row">
{t('withFrequency')}
{__('With the frequency of running it every minute:', 'mailpoet')}
</div>
<Input dimension="small" type="text" readOnly value="*/1 * * * *" />
</div>

View File

@ -115,7 +115,10 @@ export function normalizeSettings(data: Record<string, unknown>): Settings {
'Action Scheduler',
),
}),
tracking: asObject({ level: asEnum(['full', 'partial', 'basic'], 'full') }),
tracking: asObject({
level: asEnum(['full', 'partial', 'basic'], 'full'),
opens: asEnum(['merged', 'separated'], 'merged'),
}),
send_transactional_emails: disabledRadio,
deactivate_subscriber_after_inactive_days: asEnum(
['', '90', '180', '365', '540'],

View File

@ -49,6 +49,7 @@ export type Settings = {
};
tracking: {
level: 'full' | 'basic' | 'partial';
opens: 'merged' | 'separated';
};
'3rd_party_libs': {
enabled: '' | '1';

View File

@ -59,46 +59,49 @@ export function Summary({ stats, subscriber }: PropTypes): JSX.Element {
},
)}
</tr>
<tr>
<td>
<Tag>{MailPoet.I18n.t('statsMachineOpened')}</Tag>
<Tooltip
tooltip={ReactStringReplace(
MailPoet.I18n.t('statsMachineOpenedTooltip'),
/\[link](.*?)\[\/link]/,
(match) => (
<span
style={{ pointerEvents: 'all' }}
key="machine-opened-info"
>
<a
href="https://kb.mailpoet.com/article/368-what-are-machine-opens"
key="kb-link"
target="_blank"
rel="noopener noreferrer"
{MailPoet.trackingConfig.opensSeparated && (
<tr>
<td>
<Tag>{MailPoet.I18n.t('statsMachineOpened')}</Tag>
<Tooltip
tooltip={ReactStringReplace(
MailPoet.I18n.t('statsMachineOpenedTooltip'),
/\[link](.*?)\[\/link]/,
(match) => (
<span
style={{ pointerEvents: 'all' }}
key="machine-opened-info"
>
{match}
</a>
</span>
),
)}
/>
</td>
{stats.periodic_stats.map(
(periodicStats: PeriodicStats): JSX.Element => {
const displayPercentage = periodicStats.total_sent > 0;
let cell = periodicStats.machine_open.toLocaleString();
if (displayPercentage) {
const percentage = Math.round(
(periodicStats.machine_open / periodicStats.total_sent) *
100,
);
cell += ` (${percentage}%)`;
}
return <td key={periodicStats.timeframe}>{cell}</td>;
},
)}
</tr>
<a
href="https://kb.mailpoet.com/article/368-what-are-machine-opens"
key="kb-link"
target="_blank"
rel="noopener noreferrer"
>
{match}
</a>
</span>
),
)}
/>
</td>
{stats.periodic_stats.map(
(periodicStats: PeriodicStats): JSX.Element => {
const displayPercentage = periodicStats.total_sent > 0;
let cell = periodicStats.machine_open.toLocaleString();
if (displayPercentage) {
const percentage = Math.round(
(periodicStats.machine_open /
periodicStats.total_sent) *
100,
);
cell += ` (${percentage}%)`;
}
return <td key={periodicStats.timeframe}>{cell}</td>;
},
)}
</tr>
)}
<tr>
<td>
<Tag isInverted>{MailPoet.I18n.t('statsClicked')}</Tag>

View File

@ -64,6 +64,7 @@ function WooCommerceController({
MailPoet.trackingConfig.level === 'basic' ? 'basic' : 'partial';
const trackingData: Settings['tracking'] = {
level: allowed ? 'full' : trackingLevelForDisabledCookies,
opens: 'merged',
};
const subscribeOldCustomersData: Settings['mailpoet_subscribe_old_woocommerce_customers'] =
{
@ -77,6 +78,8 @@ function WooCommerceController({
// cookies allowed
'tracking.level': trackingData.level,
'woocommerce.accept_cookie_revenue_tracking.set': '1',
// human and machine opens
'tracking.opens': trackingData.opens,
};
await updateSettings(settings);
setTracking(trackingData);

View File

@ -111,7 +111,7 @@ echo '[BUILD] Removing unit tests from vendor libraries'
rm -rf $plugin_name/vendor-prefixed/cerdic/css-tidy/COPYING
rm -rf $plugin_name/vendor-prefixed/cerdic/css-tidy/NEWS
rm -rf $plugin_name/vendor-prefixed/cerdic/css-tidy/testing
rm -rf $plugin_name/vendor/mtdowling/cron-expression/tests
rm -rf $plugin_name/vendor/dragonmantank/cron-expression/tests
rm -rf $plugin_name/vendor/phpmailer/phpmailer/test
rm -rf $plugin_name/vendor-prefixed/psr/log/Psr/Log/Test
rm -rf $plugin_name/vendor-prefixed/sabberworm/php-css-parser/tests

View File

@ -1,8 +1,8 @@
{
"require": {
"php": ">=7.4",
"dragonmantank/cron-expression": "^3.3",
"mixpanel/mixpanel-php": "2.*",
"mtdowling/cron-expression": "^1.1",
"woocommerce/action-scheduler": "^3.8.0"
},
"require-dev": {

110
mailpoet/composer.lock generated
View File

@ -4,8 +4,69 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "af531d9638aae475648182d0c638008c",
"content-hash": "74b108dfa1d9c0348754b749c3e7be80",
"packages": [
{
"name": "dragonmantank/cron-expression",
"version": "v3.3.3",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
"reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
"reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0",
"webmozart/assert": "^1.0"
},
"replace": {
"mtdowling/cron-expression": "^1.0"
},
"require-dev": {
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-webmozart-assert": "^1.0",
"phpunit/phpunit": "^7.0|^8.0|^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Tankersley",
"email": "chris@ctankersley.com",
"homepage": "https://github.com/dragonmantank"
}
],
"description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
"keywords": [
"cron",
"schedule"
],
"support": {
"issues": "https://github.com/dragonmantank/cron-expression/issues",
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3"
},
"funding": [
{
"url": "https://github.com/dragonmantank",
"type": "github"
}
],
"time": "2023-08-10T19:36:49+00:00"
},
{
"name": "mixpanel/mixpanel-php",
"version": "2.11.0",
@ -56,29 +117,39 @@
"time": "2023-04-11T23:03:57+00:00"
},
{
"name": "mtdowling/cron-expression",
"version": "v1.2.3",
"name": "webmozart/assert",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/mtdowling/cron-expression.git",
"reference": "9be552eebcc1ceec9776378f7dcc085246cacca6"
"url": "https://github.com/webmozarts/assert.git",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9be552eebcc1ceec9776378f7dcc085246cacca6",
"reference": "9be552eebcc1ceec9776378f7dcc085246cacca6",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
"ext-ctype": "*",
"php": "^7.2 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "~4.0|~5.0"
"phpunit/phpunit": "^8.5.13"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -87,22 +158,21 @@
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"cron",
"schedule"
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/mtdowling/cron-expression/issues",
"source": "https://github.com/mtdowling/cron-expression/tree/v1.2.3"
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
},
"abandoned": "dragonmantank/cron-expression",
"time": "2019-12-28T04:23:06+00:00"
"time": "2022-06-03T18:03:27+00:00"
},
{
"name": "woocommerce/action-scheduler",

View File

@ -25,6 +25,7 @@ use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Options\NewsletterOptionFieldsRepository;
use MailPoet\Newsletter\Options\NewsletterOptionsRepository;
use MailPoet\Newsletter\Scheduler\AutomationEmailScheduler;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\SubscriberSegmentRepository;
use MailPoet\Subscribers\SubscribersRepository;
@ -62,29 +63,23 @@ class SendEmailAction implements Action {
'woocommerce-subscriptions:trial-started',
];
/** @var AutomationController */
private $automationController;
private AutomationController $automationController;
/** @var SettingsController */
private $settings;
private SettingsController $settings;
/** @var NewslettersRepository */
private $newslettersRepository;
private NewslettersRepository $newslettersRepository;
/** @var SubscriberSegmentRepository */
private $subscriberSegmentRepository;
private SubscriberSegmentRepository $subscriberSegmentRepository;
/** @var SubscribersRepository */
private $subscribersRepository;
private SubscribersRepository $subscribersRepository;
/** @var AutomationEmailScheduler */
private $automationEmailScheduler;
private SegmentsRepository $segmentsRepository;
/** @var NewsletterOptionsRepository */
private $newsletterOptionsRepository;
private AutomationEmailScheduler $automationEmailScheduler;
/** @var NewsletterOptionFieldsRepository */
private $newsletterOptionFieldsRepository;
private NewsletterOptionsRepository $newsletterOptionsRepository;
private NewsletterOptionFieldsRepository $newsletterOptionFieldsRepository;
public function __construct(
AutomationController $automationController,
@ -92,6 +87,7 @@ class SendEmailAction implements Action {
NewslettersRepository $newslettersRepository,
SubscriberSegmentRepository $subscriberSegmentRepository,
SubscribersRepository $subscribersRepository,
SegmentsRepository $segmentsRepository,
AutomationEmailScheduler $automationEmailScheduler,
NewsletterOptionsRepository $newsletterOptionsRepository,
NewsletterOptionFieldsRepository $newsletterOptionFieldsRepository
@ -101,6 +97,7 @@ class SendEmailAction implements Action {
$this->newslettersRepository = $newslettersRepository;
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
$this->subscribersRepository = $subscribersRepository;
$this->segmentsRepository = $segmentsRepository;
$this->automationEmailScheduler = $automationEmailScheduler;
$this->newsletterOptionsRepository = $newsletterOptionsRepository;
$this->newsletterOptionFieldsRepository = $newsletterOptionFieldsRepository;
@ -152,17 +149,20 @@ class SendEmailAction implements Action {
try {
$this->getEmailForStep($args->getStep());
} catch (InvalidStateException $exception) {
$exception = ValidationException::create()
->withMessage(__('Cannot send the email because it was not found. Please, go to the automation editor and update the email contents.', 'mailpoet'));
$emailId = $args->getStep()->getArgs()['email_id'] ?? '';
if (empty($emailId)) {
throw ValidationException::create()
->withError('email_id', __("Automation email not found.", 'mailpoet'));
}
throw ValidationException::create()
->withError(
$exception->withError('email_id', __("Automation email not found.", 'mailpoet'));
} else {
$exception->withError(
'email_id',
// translators: %s is the ID of email.
sprintf(__("Automation email with ID '%s' not found.", 'mailpoet'), $emailId)
);
}
throw $exception;
}
}
@ -174,18 +174,20 @@ class SendEmailAction implements Action {
// run #1: schedule email sending
$subscriberStatus = $subscriber->getStatus();
if ($newsletter->getType() !== NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL && $subscriberStatus !== SubscriberEntity::STATUS_SUBSCRIBED) {
throw InvalidStateException::create()->withMessage(sprintf("Cannot schedule a newsletter for subscriber ID '%s' because their status is '%s'.", $subscriber->getId(), $subscriberStatus));
// translators: %s is the subscriber's status.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber's status is '%s'.", 'mailpoet'), $subscriberStatus));
}
if ($subscriberStatus === SubscriberEntity::STATUS_BOUNCED) {
throw InvalidStateException::create()->withMessage(sprintf("Cannot schedule an email for subscriber ID '%s' because their status is '%s'.", $subscriber->getId(), $subscriberStatus));
// translators: %s is the subscriber's status.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber's status is '%s'.", 'mailpoet'), $subscriberStatus));
}
$meta = $this->getNewsletterMeta($args);
try {
$this->automationEmailScheduler->createSendingTask($newsletter, $subscriber, $meta);
} catch (Throwable $e) {
throw InvalidStateException::create()->withMessage('Could not create sending task.');
throw InvalidStateException::create()->withMessage(__('Could not create sending task.', 'mailpoet'));
}
} else {
@ -207,21 +209,24 @@ class SendEmailAction implements Action {
public function handleEmailSent($data): void {
if (!is_array($data)) {
throw InvalidStateException::create()->withMessage(
sprintf('Invalid automation step data. Array expected, got: %s', gettype($data))
// translators: %s is the type of $data.
sprintf(__('Invalid automation step data. Array expected, got: %s', 'mailpoet'), gettype($data))
);
}
$runId = $data['run_id'] ?? null;
if (!is_int($runId)) {
throw InvalidStateException::create()->withMessage(
sprintf("Invalid automation step data. Expected 'run_id' to be an integer, got: %s", gettype($runId))
// translators: %s is the type of $runId.
sprintf(__("Invalid automation step data. Expected 'run_id' to be an integer, got: %s", 'mailpoet'), gettype($runId))
);
}
$stepId = $data['step_id'] ?? null;
if (!is_string($stepId)) {
throw InvalidStateException::create()->withMessage(
sprintf("Invalid automation step data. Expected 'step_id' to be a string, got: %s", gettype($runId))
// translators: %s is the type of $runId.
sprintf(__("Invalid automation step data. Expected 'step_id' to be a string, got: %s", 'mailpoet'), gettype($runId))
);
}
@ -231,13 +236,14 @@ class SendEmailAction implements Action {
private function checkSendingStatus(StepRunArgs $args, NewsletterEntity $newsletter, SubscriberEntity $subscriber): bool {
$scheduledTaskSubscriber = $this->automationEmailScheduler->getScheduledTaskSubscriber($newsletter, $subscriber, $args->getAutomationRun());
if (!$scheduledTaskSubscriber) {
throw InvalidStateException::create()->withMessage('Email failed to schedule.');
throw InvalidStateException::create()->withMessage(__('Email failed to schedule.', 'mailpoet'));
}
// email sending failed
if ($scheduledTaskSubscriber->getFailed() === ScheduledTaskSubscriberEntity::FAIL_STATUS_FAILED) {
throw InvalidStateException::create()->withMessage(
sprintf('Email failed to send. Error: %s', $scheduledTaskSubscriber->getError() ?: 'Unknown error')
// translators: %s is the error message.
sprintf(__('Email failed to send. Error: %s', 'mailpoet'), $scheduledTaskSubscriber->getError() ?: 'Unknown error')
);
}
@ -246,7 +252,7 @@ class SendEmailAction implements Action {
// email was never sent
if (!$wasSent && $isLastRun) {
$error = 'Email sending process timed out.';
$error = __('Email sending process timed out.', 'mailpoet');
$this->automationEmailScheduler->saveError($scheduledTaskSubscriber, $error);
throw InvalidStateException::create()->withMessage($error);
}
@ -298,7 +304,12 @@ class SendEmailAction implements Action {
]);
if (!$subscriberSegment) {
throw InvalidStateException::create()->withMessage(sprintf("Subscriber ID '%s' is not subscribed to segment ID '%s'.", $subscriberId, $segmentId));
$segment = $this->segmentsRepository->findOneById($segmentId);
if (!$segment) { // This state should not happen because it is checked in the validation.
throw InvalidStateException::create()->withMessage(__('Cannot send the email because the list was not found.', 'mailpoet'));
}
// translators: %s is the name of the list.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber is not subscribed to the '%s' list.", 'mailpoet'), $segment->getName()));
}
$subscriber = $subscriberSegment->getSubscriber();

View File

@ -13,6 +13,7 @@ use MailPoet\Cron\Workers\StatsNotifications\Worker;
use MailPoet\Cron\Workers\SubscriberLinkTokens;
use MailPoet\Cron\Workers\SubscribersLastEngagement;
use MailPoet\Cron\Workers\UnsubscribeTokens;
use MailPoet\Doctrine\WPDB\Connection;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\ScheduledTaskEntity;
@ -610,6 +611,21 @@ class Populator {
$conditions = implode(' AND ', $conditions);
$table = esc_sql($table);
// SQLite doesn't support JOIN in DELETE queries, we need to use a subquery.
if (Connection::isSQLite()) {
return $wpdb->query(
$wpdb->prepare(
"DELETE FROM $table WHERE id IN (
SELECT t1.id
FROM $table t1
JOIN $table t2 ON t1.id < t2.id AND $conditions
)",
$values
)
);
}
return $wpdb->query(
$wpdb->prepare(
"DELETE t1 FROM $table t1, $table t2 WHERE t1.id < t2.id AND $conditions",
@ -622,6 +638,12 @@ class Populator {
$statisticsFormTable = $this->entityManager->getClassMetadata(StatisticsFormEntity::class)->getTableName();
$subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
// Temporarily skip the queries in WP Playground.
// UPDATE with JOIN is not yet supported by the SQLite integration.
if (Connection::isSQLite()) {
return;
}
$this->entityManager->getConnection()->executeStatement(
' UPDATE LOW_PRIORITY `' . $subscriberTable . '` subscriber ' .
' JOIN `' . $statisticsFormTable . '` stats ON stats.subscriber_id=subscriber.id ' .

View File

@ -7,6 +7,9 @@ use MailPoet\Doctrine\WPDB\Exceptions\QueryException;
use MailPoetVendor\Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use MailPoetVendor\Doctrine\DBAL\ParameterType;
use mysqli;
use PDO;
use PDOException;
use Throwable;
use wpdb;
/**
@ -78,27 +81,56 @@ class Connection implements ServerInfoAwareConnection {
return $wpdb->db_server_info();
}
/** @return mysqli|false|null */
/**
* MySQL — returns an instance of mysqli.
* SQLite — returns an instance of PDO.
*
* @return mysqli|PDO|false|null
*/
public function getNativeConnection() {
global $wpdb;
// WPDB keeps connection instance (mysqli) in a protected property $dbh.
// We can access it using a closure that is bound to the $wpdb instance.
$getConnection = function () {
$getDbh = function () {
return $this->dbh; // @phpstan-ignore-line -- PHPStan doesn't know the binding context
};
return $getConnection->call($wpdb);
$dbh = $getDbh->call($wpdb);
if (is_object($dbh) && method_exists($dbh, 'get_pdo')) {
return $dbh->get_pdo();
}
return $getDbh->call($wpdb);
}
public static function isSQLite(): bool {
return defined('DB_ENGINE') && DB_ENGINE === 'sqlite';
}
private function runQuery(string $sql) {
global $wpdb;
$value = $wpdb->query($sql); // phpcs:ignore WordPressDotOrg.sniffs.DirectDB.UnescapedDBParameter
try {
$value = $wpdb->query($sql); // phpcs:ignore WordPressDotOrg.sniffs.DirectDB.UnescapedDBParameter
} catch (Throwable $e) {
if ($e instanceof PDOException) {
throw new QueryException($e->getMessage(), $e->errorInfo[0] ?? null, $e->errorInfo[1] ?? 0);
}
throw new QueryException($e->getMessage(), null, 0, $e);
}
if ($value === false) {
$nativeConnection = $this->getNativeConnection();
$sqlState = $nativeConnection instanceof mysqli ? $nativeConnection->sqlstate : null;
$code = $nativeConnection instanceof mysqli ? $nativeConnection->errno : 0;
throw new QueryException($wpdb->last_error, $sqlState, $code);
$this->handleQueryError();
}
return $value;
}
private function handleQueryError(): void {
global $wpdb;
$nativeConnection = $this->getNativeConnection();
if ($nativeConnection instanceof mysqli) {
throw new QueryException($wpdb->last_error, $nativeConnection->sqlstate, $nativeConnection->errno);
} elseif ($nativeConnection instanceof PDO) {
$info = $nativeConnection->errorInfo();
throw new QueryException($wpdb->last_error, $info[0] ?? null, $info[1] ?? 0);
}
throw new QueryException($wpdb->last_error);
}
}

View File

@ -2,6 +2,7 @@
namespace MailPoet\Migrations\App;
use MailPoet\Doctrine\WPDB\Connection;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
@ -100,6 +101,12 @@ class Migration_20240207_105912_App extends AppMigration {
$scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
$scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
$sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
// Temporarily skip the query in WP Playground.
// UPDATE with JOIN is not yet supported by the SQLite integration.
if (Connection::isSQLite()) {
return;
}
$this->entityManager->getConnection()->executeStatement(
"
UPDATE $newslettersTable n
@ -166,6 +173,12 @@ class Migration_20240207_105912_App extends AppMigration {
$scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
$scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
$sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
// Temporarily skip the query in WP Playground.
// UPDATE with JOIN is not yet supported by the SQLite integration.
if (Connection::isSQLite()) {
return;
}
$this->entityManager->getConnection()->executeStatement(
"
UPDATE $newslettersTable n

View File

@ -691,11 +691,7 @@ class Migration_20221028_105818 extends DbMigration {
$wpdb->query($updateCreatedAtQuery);
// Add updated_at column in case it doesn't exist
$updatedAtColumnExists = $wpdb->get_results($wpdb->prepare("
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = %s AND column_name = 'updated_at';
", $scheduledTasksSubscribersTable));
$updatedAtColumnExists = $this->columnExists($scheduledTasksSubscribersTable, 'updated_at');
if (empty($updatedAtColumnExists)) {
$addUpdatedAtQuery = "
ALTER TABLE `$scheduledTasksSubscribersTable`
@ -719,13 +715,7 @@ class Migration_20221028_105818 extends DbMigration {
esc_sql("{$this->prefix}statistics_opens"),
];
foreach ($statisticsTables as $statisticsTable) {
$oldStatisticsIndexExists = $wpdb->get_results($wpdb->prepare("
SELECT DISTINCT INDEX_NAME
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = %s
AND TABLE_NAME = %s
AND INDEX_NAME='newsletter_id_subscriber_id'
", $dbName, $statisticsTable));
$oldStatisticsIndexExists = $this->indexExists($statisticsTable, 'newsletter_id_subscriber_id');
if (!empty($oldStatisticsIndexExists)) {
$dropIndexQuery = "
ALTER TABLE `{$statisticsTable}`
@ -1027,13 +1017,7 @@ class Migration_20221028_105818 extends DbMigration {
return;
}
$premiumTableName = $wpdb->prefix . 'mailpoet_premium_newsletter_extra_data';
$premiumTableExists = (int)$wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(1) FROM information_schema.tables WHERE table_schema=%s AND table_name=%s;",
$wpdb->dbname,
$premiumTableName
)
);
$premiumTableExists = $this->tableExists($premiumTableName);
if ($premiumTableExists) {
$table = esc_sql($this->getTableName(NewsletterEntity::class));
$query = "

View File

@ -2,6 +2,7 @@
namespace MailPoet\Migrations\Db;
use MailPoet\Doctrine\WPDB\Connection;
use MailPoet\Migrator\DbMigration;
class Migration_20230831_143755_Db extends DbMigration {
@ -18,19 +19,40 @@ class Migration_20230831_143755_Db extends DbMigration {
// add "step_type" column
if (!$this->columnExists($logsTable, 'step_type')) {
$this->connection->executeStatement("ALTER TABLE $logsTable ADD COLUMN step_type VARCHAR(255) NOT NULL DEFAULT 'action' AFTER `step_id`");
$this->connection->executeStatement("ALTER TABLE $logsTable ALTER COLUMN step_type DROP DEFAULT");
// Temporarily use a full column definition to drop default in WP Playground.
// DROP DEFAULT is not yet supported by the SQLite integration.
if (Connection::isSQLite()) {
$this->connection->executeStatement("ALTER TABLE $logsTable CHANGE COLUMN step_type step_type VARCHAR(255) NOT NULL");
} else {
$this->connection->executeStatement("ALTER TABLE $logsTable ALTER COLUMN step_type DROP DEFAULT");
}
}
// add "step_key" column
if (!$this->columnExists($logsTable, 'step_key')) {
$this->connection->executeStatement("ALTER TABLE $logsTable ADD COLUMN step_key VARCHAR(255) NOT NULL DEFAULT '' AFTER `step_type`");
$this->connection->executeStatement("ALTER TABLE $logsTable ALTER COLUMN step_key DROP DEFAULT");
// Temporarily use a full column definition to drop default in WP Playground.
// DROP DEFAULT is not yet supported by the SQLite integration.
if (Connection::isSQLite()) {
$this->connection->executeStatement("ALTER TABLE $logsTable CHANGE COLUMN step_key step_key VARCHAR(255) NOT NULL");
} else {
$this->connection->executeStatement("ALTER TABLE $logsTable ALTER COLUMN step_key DROP DEFAULT");
}
}
// add "run_number" column
if (!$this->columnExists($logsTable, 'run_number')) {
$this->connection->executeStatement("ALTER TABLE $logsTable ADD COLUMN run_number INT NOT NULL DEFAULT 1 AFTER `updated_at`");
$this->connection->executeStatement("ALTER TABLE $logsTable ALTER COLUMN run_number DROP DEFAULT");
// Temporarily use a full column definition to drop default in WP Playground.
// DROP DEFAULT is not yet supported by the SQLite integration.
if (Connection::isSQLite()) {
$this->connection->executeStatement("ALTER TABLE $logsTable CHANGE COLUMN run_number run_number INT NOT NULL");
} else {
$this->connection->executeStatement("ALTER TABLE $logsTable ALTER COLUMN run_number DROP DEFAULT");
}
}
// go through automation data and backfill step keys and trigger logs

View File

@ -5,6 +5,7 @@ namespace MailPoet\Migrator;
use MailPoet\Config\Env;
use MailPoet\DI\ContainerWrapper;
use MailPoetVendor\Doctrine\DBAL\Connection;
use MailPoetVendor\Doctrine\DBAL\Exception;
use MailPoetVendor\Doctrine\ORM\EntityManager;
abstract class DbMigration {
@ -42,35 +43,42 @@ abstract class DbMigration {
}
protected function columnExists(string $tableName, string $columnName): bool {
// We had a problem with the dbName value in ENV for some customers, because it doesn't match DB name in information schema.
// So we decided to use the DATABASE() value instead.
return $this->connection->executeQuery("
SELECT 1
FROM information_schema.columns
WHERE table_schema = COALESCE(DATABASE(), ?)
AND table_name = ?
AND column_name = ?
", [Env::$dbName, $tableName, $columnName])->fetchOne() !== false;
global $wpdb;
$suppressErrors = $wpdb->suppress_errors();
try {
$this->connection->executeStatement("SELECT $columnName FROM $tableName LIMIT 0");
return true;
} catch (Exception $e) {
return false;
} finally {
$wpdb->suppress_errors($suppressErrors);
}
}
protected function tableExists(string $tableName): bool {
return $this->connection->executeQuery("
SELECT 1
FROM information_schema.columns
WHERE table_schema = COALESCE(DATABASE(), ?)
AND table_name = ?
", [Env::$dbName, $tableName])->fetchOne() !== false;
global $wpdb;
$suppressErrors = $wpdb->suppress_errors();
try {
$this->connection->executeStatement("SELECT 1 FROM $tableName LIMIT 0");
return true;
} catch (Exception $e) {
return false;
} finally {
$wpdb->suppress_errors($suppressErrors);
}
}
protected function indexExists(string $tableName, string $indexName): bool {
// We had a problem with the dbName value in ENV for some customers, because it doesn't match DB name in information schema.
// So we decided to use the DATABASE() value instead.
return $this->connection->executeQuery("
SELECT 1
FROM information_schema.statistics
WHERE table_schema = COALESCE(DATABASE(), ?)
AND table_name = ?
AND index_name = ?
", [Env::$dbName, $tableName, $indexName])->fetchOne() !== false;
global $wpdb;
$suppressErrors = $wpdb->suppress_errors();
try {
$this->connection->executeStatement("ALTER TABLE $tableName ADD INDEX $indexName (__non__existent__column__name__)");
} catch (Exception $e) {
// Index exists when the error message contains its name. Otherwise, it's the non-existent column error.
return strpos($e->getMessage(), $indexName) !== false;
} finally {
$wpdb->suppress_errors($suppressErrors);
}
return false;
}
}

View File

@ -39,7 +39,7 @@ class Scheduler {
// 3) We convert the calculated time to UTC
$from = $this->wp->currentDatetime();
try {
$schedule = \Cron\CronExpression::factory($schedule);
$schedule = new \Cron\CronExpression((string)$schedule);
$previousRunDate = $schedule->getPreviousRunDate(Carbon::instance($from));
$previousRunDate->setTimezone(new \DateTimeZone('UTC'));
$previousRunDate = $previousRunDate->format('Y-m-d H:i:s');
@ -94,9 +94,13 @@ class Scheduler {
//$fromTimestamp = $this->wp->currentTime('timestamp', false);
$from = $this->wp->currentDatetime();
try {
$schedule = \Cron\CronExpression::factory($schedule);
$schedule = new \Cron\CronExpression((string)$schedule);
$nextRunDate = $schedule->getNextRunDate(Carbon::instance($from));
$nextRunDate->setTimezone(new \DateTimeZone('UTC'));
// Work around CronExpression transforming Carbon into DateTime
if (!$nextRunDate instanceof Carbon) {
$nextRunDate = new Carbon($nextRunDate);
}
} catch (\Exception $e) {
$nextRunDate = false;
}

View File

@ -14,6 +14,7 @@ use MailPoet\Entities\StatisticsUnsubscribeEntity;
use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\UserAgentEntity;
use MailPoet\Settings\TrackingConfig;
use MailPoet\WooCommerce\Helper as WCHelper;
use MailPoetVendor\Doctrine\ORM\EntityManager;
use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
@ -28,12 +29,17 @@ class NewsletterStatisticsRepository extends Repository {
/** @var WCHelper */
private $wcHelper;
/** @var TrackingConfig */
private $trackingConfig;
public function __construct(
EntityManager $entityManager,
WCHelper $wcHelper
WCHelper $wcHelper,
TrackingConfig $trackingConfig
) {
parent::__construct($entityManager);
$this->wcHelper = $wcHelper;
$this->trackingConfig = $trackingConfig;
}
protected function getEntityClassName() {
@ -222,7 +228,10 @@ class NewsletterStatisticsRepository extends Repository {
private function getStatisticCounts(string $statisticsEntityName, array $newsletters, \DateTimeImmutable $from = null, \DateTimeImmutable $to = null): array {
$qb = $this->getStatisticsQuery($statisticsEntityName, $newsletters);
if (in_array($statisticsEntityName, [StatisticsOpenEntity::class, StatisticsClickEntity::class], true)) {
if (
$statisticsEntityName === StatisticsClickEntity::class
|| ($statisticsEntityName === StatisticsOpenEntity::class && $this->trackingConfig->areOpensSeparated())
) {
$qb->andWhere('(stats.userAgentType = :userAgentType) OR (stats.userAgentType IS NULL)')
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN);
}

View File

@ -4,6 +4,7 @@ namespace MailPoet\Segments;
use MailPoet\Config\SubscriberChangesNotifier;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Doctrine\WPDB\Connection;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
@ -246,6 +247,12 @@ class WP {
}
public function synchronizeUsers(): bool {
// Temporarily skip synchronization in WP Playground.
// Some of the queries are not yet supported by the SQLite integration.
if (Connection::isSQLite()) {
return true;
}
// Save timestamp about changes and update before insert
$this->subscriberChangesNotifier->subscribersBatchCreate();
$this->subscriberChangesNotifier->subscribersBatchUpdate();

View File

@ -7,6 +7,9 @@ class TrackingConfig {
const LEVEL_PARTIAL = 'partial';
const LEVEL_BASIC = 'basic';
const OPENS_MERGED = 'merged';
const OPENS_SEPARATED = 'separated';
/** @var SettingsController */
private $settings;
@ -26,11 +29,23 @@ class TrackingConfig {
return $level === self::LEVEL_FULL;
}
public function areOpensMerged(string $opens = null): bool {
$opens = $opens ?? $this->settings->get('tracking.opens', self::OPENS_MERGED);
return $opens !== self::OPENS_SEPARATED;
}
public function areOpensSeparated(string $opens = null): bool {
return !$this->areOpensMerged($opens);
}
public function getConfig(): array {
return [
'level' => $this->settings->get('tracking.level', self::LEVEL_FULL),
'emailTrackingEnabled' => $this->isEmailTrackingEnabled(),
'cookieTrackingEnabled' => $this->isCookieTrackingEnabled(),
'opens' => $this->settings->get('tracking.opens', self::OPENS_MERGED),
'opensMerged' => $this->areOpensMerged(),
'opensSeparated' => $this->areOpensSeparated(),
];
}
}

View File

@ -10,6 +10,7 @@ use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\UserAgentEntity;
use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
use MailPoet\Settings\TrackingConfig;
use MailPoet\WooCommerce\Helper as WCHelper;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager;
@ -23,12 +24,17 @@ class SubscriberStatisticsRepository extends Repository {
/** @var WCHelper */
private $wcHelper;
/** @var TrackingConfig */
private $trackingConfig;
public function __construct(
EntityManager $entityManager,
WCHelper $wcHelper
WCHelper $wcHelper,
TrackingConfig $trackingConfig
) {
parent::__construct($entityManager);
$this->wcHelper = $wcHelper;
$this->trackingConfig = $trackingConfig;
}
protected function getEntityClassName() {
@ -64,9 +70,13 @@ class SubscriberStatisticsRepository extends Repository {
}
public function getStatisticsOpenCount(SubscriberEntity $subscriber, ?Carbon $startTime = null): int {
return (int)$this->getStatisticsOpenCountQuery($subscriber, $startTime)
->andWhere('(stats.userAgentType = :userAgentType)')
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN)
$queryBuilder = $this->getStatisticsOpenCountQuery($subscriber, $startTime);
if ($this->trackingConfig->areOpensSeparated()) {
$queryBuilder
->andWhere('(stats.userAgentType = :userAgentType)')
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN);
}
return (int)$queryBuilder
->getQuery()
->getSingleScalarResult();
}

View File

@ -0,0 +1,113 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
use MailPoet\Config\Env;
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 || $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);
}
}

View File

@ -10,6 +10,7 @@ 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 {
@ -64,8 +65,12 @@ class PermanentNotices {
/** @var SenderDomainAuthenticationNotices */
private $senderDomainAuthenticationNotices;
/** @var DatabaseEngineNotice */
private $databaseEngineNotice;
public function __construct(
WPFunctions $wp,
EntityManager $entityManager,
TrackingConfig $trackingConfig,
SubscribersRepository $subscribersRepository,
SettingsController $settings,
@ -90,6 +95,7 @@ class PermanentNotices {
$this->pendingApprovalNotice = new PendingApprovalNotice($settings);
$this->woocommerceVersionWarning = new WooCommerceVersionWarning($wp);
$this->premiumFeaturesAvailableNotice = new PremiumFeaturesAvailableNotice($subscribersFeature, $serviceChecker, $wp);
$this->databaseEngineNotice = new DatabaseEngineNotice($wp, $entityManager);
$this->senderDomainAuthenticationNotices = $senderDomainAuthenticationNotices;
}
@ -150,6 +156,9 @@ class PermanentNotices {
$this->premiumFeaturesAvailableNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$this->databaseEngineNotice->init(
Menu::isOnMailPoetAdminPage($excludeSetupWizard)
);
$excludeDomainAuthenticationNotices = [
'mailpoet-settings',
'mailpoet-newsletter-editor',
@ -193,6 +202,9 @@ class PermanentNotices {
case (WooCommerceVersionWarning::OPTION_NAME):
$this->woocommerceVersionWarning->disable();
break;
case (DatabaseEngineNotice::OPTION_NAME):
$this->databaseEngineNotice->disable();
break;
case (PremiumFeaturesAvailableNotice::OPTION_NAME):
$this->premiumFeaturesAvailableNotice->disable();
break;

View File

@ -2,7 +2,7 @@
/*
* Plugin Name: MailPoet
* Version: 5.0.1
* Version: 5.0.2
* Plugin URI: https://www.mailpoet.com
* Description: Create and send newsletters, post notifications and welcome emails from your WordPress.
* Author: MailPoet
@ -20,7 +20,7 @@
*/
$mailpoetPlugin = [
'version' => '5.0.1',
'version' => '5.0.2',
'filename' => __FILE__,
'path' => dirname(__FILE__),
'autoloader' => dirname(__FILE__) . '/vendor/autoload.php',

View File

@ -3,7 +3,7 @@ Contributors: mailpoet, woocommerce, automattic
Tags: email marketing, post notification, woocommerce emails, email automation, newsletter
Requires at least: 6.5
Tested up to: 6.6
Stable tag: 5.0.1
Stable tag: 5.0.2
Requires PHP: 7.4
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -138,6 +138,7 @@ Please note:
* Persian
* Polish
* Romanian
* Ukrainian
We welcome experienced translators to translate directly on [our Transifex project](https://www.transifex.com/wysija/mp3/). Please note that any translations submitted via the "Translating WordPress" website will not work.
@ -229,7 +230,9 @@ Check our [Knowledge Base](https://kb.mailpoet.com) or contact us through our [s
== Changelog ==
= 5.0.1 - 2024-08-23 =
* Fixed: incorrect date in the scheduling calendar on the send page.
= 5.0.2 - 2024-08-26 =
* Added: Ukrainian translations;
* Improved: error messages in automations;
* Changed: human and machine opens are merged by default, old behavior can be restored in settings.
[See the changelog for all versions.](https://github.com/mailpoet/mailpoet/blob/trunk/mailpoet/CHANGELOG.md)

View File

@ -148,7 +148,7 @@ parameters:
-
message: "#^Part \\$table \\(array\\|string\\) of encapsed string cannot be cast to string\\.$#"
count: 2
count: 5
path: ../../lib/Config/Populator.php
-

View File

@ -148,7 +148,7 @@ parameters:
-
message: "#^Part \\$table \\(array\\|string\\) of encapsed string cannot be cast to string\\.$#"
count: 2
count: 5
path: ../../lib/Config/Populator.php
-

View File

@ -77,7 +77,7 @@ class WelcomeWizardCest {
Assert::assertSame($senderAddress, $this->findSetting('sender')->getValue()['address']);
Assert::assertSame(['enabled' => '1'], $this->findSetting('3rd_party_libs')->getValue());
Assert::assertSame(['enabled' => '1'], $this->findSetting('analytics')->getValue());
Assert::assertSame(['level' => 'full'], $this->findSetting('tracking')->getValue());
Assert::assertSame(['level' => 'full', 'opens' => 'merged'], $this->findSetting('tracking')->getValue());
Assert::assertSame($mailPoetSendingKey, $this->findSetting('mta')->getValue()['mailpoet_api_key']);
}
@ -118,7 +118,7 @@ class WelcomeWizardCest {
Assert::assertSame($senderAddress, $this->findSetting('sender')->getValue()['address']);
Assert::assertSame(['enabled' => '1'], $this->findSetting('3rd_party_libs')->getValue());
Assert::assertSame(['enabled' => '1'], $this->findSetting('analytics')->getValue());
Assert::assertSame(['level' => 'full'], $this->findSetting('tracking')->getValue());
Assert::assertSame(['level' => 'full', 'opens' => 'merged'], $this->findSetting('tracking')->getValue());
Assert::assertSame($mailPoetSendingKey, $this->findSetting('mta')->getValue()['mailpoet_api_key']);
}

View File

@ -30,6 +30,7 @@ use MailPoet\Router\Router;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Test\DataFactories\Newsletter;
use MailPoet\Test\DataFactories\NewsletterOption;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
@ -94,7 +95,8 @@ class NewslettersTest extends \MailPoetTest {
$this->diContainer->get(NewslettersRepository::class),
new NewsletterStatisticsRepository(
$this->diContainer->get(EntityManager::class),
$this->makeEmpty(WCHelper::class)
$this->makeEmpty(WCHelper::class),
$this->diContainer->get(TrackingConfig::class)
),
$this->diContainer->get(Url::class),
$this->diContainer->get(SendingQueuesRepository::class),

View File

@ -101,13 +101,23 @@ class ConnectionTest extends MailPoetTest {
}
public function testTransactionRollBack(): void {
global $wpdb;
$tableStatus = (array)$wpdb->get_row($wpdb->prepare("SHOW TABLE STATUS LIKE %s", self::TEST_TABLE_NAME));
$connection = new Connection();
$this->assertTrue($connection->beginTransaction());
$connection->exec(sprintf("INSERT INTO %s (value) VALUES ('test')", self::TEST_TABLE_NAME));
$this->assertTrue($connection->rollBack());
$this->assertSame('0', $connection->query(sprintf('SELECT COUNT(*) FROM %s', self::TEST_TABLE_NAME))->fetchOne());
$rowCount = $connection->query(sprintf('SELECT COUNT(*) FROM %s', self::TEST_TABLE_NAME))->fetchOne();
if ($tableStatus['Engine'] === 'MyISAM') {
// MyISAM does not support transactions. It ignores transaction commands as noop, so the insert is not rolled back.
$this->assertSame('1', $rowCount);
} else {
$this->assertSame('0', $rowCount);
}
}
public function testQuote(): void {

View File

@ -8,10 +8,13 @@ use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Statistics\StatisticsWooCommercePurchasesRepository;
use MailPoet\Test\DataFactories\Newsletter;
use MailPoet\Test\DataFactories\NewsletterLink;
use MailPoet\Test\DataFactories\StatisticsClicks;
use MailPoet\Test\DataFactories\StatisticsOpens;
use MailPoet\Test\DataFactories\Subscriber;
/**
@ -32,6 +35,9 @@ class NewsletterStatisticsRepositoryTest extends \MailPoetTest {
/** @var \MailPoet\Entities\SubscriberEntity */
private $subscriber;
/** @var \MailPoet\Entities\SubscriberEntity */
private $subscriber2;
/** @var StatisticsClickEntity */
private $click1;
@ -44,6 +50,7 @@ class NewsletterStatisticsRepositoryTest extends \MailPoetTest {
$this->newsletter = (new Newsletter())->withSendingQueue()->create();
$this->assertInstanceOf(NewsletterEntity::class, $this->newsletter);
$this->subscriber = (new Subscriber())->create();
$this->subscriber2 = (new Subscriber())->create();
$link = (new NewsletterLink($this->newsletter))->create();
$this->click1 = (new StatisticsClicks($link, $this->subscriber))->create();
@ -51,6 +58,22 @@ class NewsletterStatisticsRepositoryTest extends \MailPoetTest {
$this->click2 = (new StatisticsClicks($link, $this->subscriber))->create();
}
public function testItGetsMergedOpens() {
$open = (new StatisticsOpens($this->newsletter, $this->subscriber))->create();
$open2 = (new StatisticsOpens($this->newsletter, $this->subscriber2))->withMachineUserAgentType()->create();
SettingsController::getInstance()->set('tracking.opens', TrackingConfig::OPENS_MERGED);
$count = $this->testee->getStatisticsOpenCount($this->newsletter);
verify($count)->equals(2);
}
public function testItGetsSeparatedOpens() {
$open = (new StatisticsOpens($this->newsletter, $this->subscriber))->create();
$open2 = (new StatisticsOpens($this->newsletter, $this->subscriber2))->withMachineUserAgentType()->create();
SettingsController::getInstance()->set('tracking.opens', TrackingConfig::OPENS_SEPARATED);
$count = $this->testee->getStatisticsOpenCount($this->newsletter);
verify($count)->equals(1);
}
public function testItGetsOnlyStatisticsWithTheCorrectStatus() {
$queue = $this->newsletter->getLatestQueue();
$this->assertInstanceOf(SendingQueueEntity::class, $queue);

View File

@ -3,6 +3,8 @@
namespace MailPoet\Subscribers\Statistics;
use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Test\DataFactories\Newsletter;
use MailPoet\Test\DataFactories\NewsletterLink;
use MailPoet\Test\DataFactories\StatisticsClicks;
@ -19,9 +21,13 @@ class SubscriberStatisticsRepositoryTest extends \MailPoetTest {
/** @var SubscriberStatisticsRepository */
private $repository;
/** @var SettingsController */
private $settings;
public function _before() {
parent::_before();
$this->repository = $this->diContainer->get(SubscriberStatisticsRepository::class);
$this->settings = SettingsController::getInstance();
}
public function testItFetchesClickCount(): void {
@ -77,6 +83,42 @@ class SubscriberStatisticsRepositoryTest extends \MailPoetTest {
verify($this->repository->getStatisticsMachineOpenCount($subscriber, null))->equals(0);
}
public function testItFetchesOpenCountMergedWithMachineCount(): void {
$subscriber = (new Subscriber())->create();
$newsletter = (new Newsletter())->withSendingQueue()->create();
$newsletter2 = (new Newsletter())->withSendingQueue()->create();
$yearAgo = Carbon::now()->subYear();
$open = (new StatisticsOpens($newsletter, $subscriber))->withCreatedAt($yearAgo)->create();
$open2 = (new StatisticsOpens($newsletter2, $subscriber))->withMachineUserAgentType()->withCreatedAt($yearAgo)->create();
$newsletterSendStat = (new StatisticsNewsletters($newsletter, $subscriber))->withSentAt($yearAgo)->create();
$newsletterSendStat2 = (new StatisticsNewsletters($newsletter2, $subscriber))->withSentAt($yearAgo)->create();
$this->settings->set('tracking.opens', TrackingConfig::OPENS_MERGED);
verify($this->repository->getStatisticsOpenCount($subscriber, null))->equals(2);
verify($this->repository->getStatisticsOpenCount($subscriber, $yearAgo))->equals(2);
verify($this->repository->getStatisticsOpenCount($subscriber, Carbon::now()->subMonth()))->equals(0);
verify($this->repository->getStatisticsMachineOpenCount($subscriber, null))->equals(1);
}
public function testItFetchesOpenCountSeparatedFromMachineCount(): void {
$subscriber = (new Subscriber())->create();
$newsletter = (new Newsletter())->withSendingQueue()->create();
$newsletter2 = (new Newsletter())->withSendingQueue()->create();
$yearAgo = Carbon::now()->subYear();
$open = (new StatisticsOpens($newsletter, $subscriber))->withCreatedAt($yearAgo)->create();
$open2 = (new StatisticsOpens($newsletter2, $subscriber))->withMachineUserAgentType()->withCreatedAt($yearAgo)->create();
$newsletterSendStat = (new StatisticsNewsletters($newsletter, $subscriber))->withSentAt($yearAgo)->create();
$newsletterSendStat2 = (new StatisticsNewsletters($newsletter2, $subscriber))->withSentAt($yearAgo)->create();
$this->settings->set('tracking.opens', TrackingConfig::OPENS_SEPARATED);
verify($this->repository->getStatisticsOpenCount($subscriber, null))->equals(1);
verify($this->repository->getStatisticsOpenCount($subscriber, $yearAgo))->equals(1);
verify($this->repository->getStatisticsOpenCount($subscriber, Carbon::now()->subMonth()))->equals(0);
verify($this->repository->getStatisticsMachineOpenCount($subscriber, null))->equals(1);
}
public function testItFetchesMachineOpenCount(): void {
$subscriber = (new Subscriber())->create();
$newsletter = (new Newsletter())->withSendingQueue()->create();
@ -87,7 +129,9 @@ class SubscriberStatisticsRepositoryTest extends \MailPoetTest {
verify($this->repository->getStatisticsMachineOpenCount($subscriber, null))->equals(1);
verify($this->repository->getStatisticsMachineOpenCount($subscriber, $yearAgo))->equals(1);
verify($this->repository->getStatisticsMachineOpenCount($subscriber, Carbon::now()->subMonth()))->equals(0);
verify($this->repository->getStatisticsOpenCount($subscriber, null))->equals(0);
verify($this->repository->getStatisticsOpenCount($subscriber, null))->equals(1); // Merged with machine count
$this->settings->set('tracking.opens', TrackingConfig::OPENS_SEPARATED);
verify($this->repository->getStatisticsOpenCount($subscriber, null))->equals(0); // Separated from machine count
}
public function testItFetchesTotalSentCount(): void {

View File

@ -0,0 +1,90 @@
<?php declare(strict_types = 1);
namespace MailPoet\Util\Notices;
use MailPoet\Entities\FormEntity;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class DatabaseEngineNoticeTest extends \MailPoetTest {
/** @var DatabaseEngineNotice */
private $notice;
private $tableName;
public function _before() {
parent::_before();
$wp = new WPFunctions();
$wp->deleteTransient(DatabaseEngineNotice::OPTION_NAME . '-cache');
$this->tableName = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
$this->notice = new DatabaseEngineNotice(
$wp,
$this->entityManager
);
}
public function _after() {
$this->entityManager->getConnection()->executeStatement("
ALTER TABLE {$this->tableName}
ENGINE = INNODB;
");
parent::_after();
}
public function testItDisplaysNoticeWhenMyISAMDetected() {
$this->entityManager->getConnection()->executeStatement("
ALTER TABLE {$this->tableName}
ENGINE = MyISAM;
");
$result = $this->notice->init(true);
$this->assertInstanceOf(Notice::class, $result);
$message = $result->getMessage();
verify($message)->stringContainsString('Some of the MailPoet plugins tables are not using the InnoDB engine');
verify($message)->stringContainsString('https://kb.mailpoet.com/article/200-solving-database-connection-issues#database-configuration');
verify($message)->stringContainsString($this->tableName);
}
public function testItDisplaysNoticeWithMultipleTables() {
$tables = [
$this->entityManager->getClassMetadata(FormEntity::class)->getTableName(),
$this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName(),
$this->tableName,
$this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(),
];
foreach ($tables as $table) {
$this->entityManager->getConnection()->executeStatement("ALTER TABLE {$table} ENGINE = MyISAM;");
}
$result = $this->notice->init(true);
$this->assertInstanceOf(Notice::class, $result);
$message = $result->getMessage();
verify($message)->stringContainsString('and 2 more');
verify($message)->stringContainsString('“' . $tables[0] . '”');
foreach ($tables as $table) {
$this->entityManager->getConnection()->executeStatement("ALTER TABLE {$table} ENGINE = INNODB;");
}
}
public function testItCallsOnlyOnce() {
$connection = $this->entityManager->getConnection();
$entityManager = $this->createMock(EntityManager::class);
$entityManager->expects($this->once())->method('getConnection')->willReturn($connection);
$notice = new DatabaseEngineNotice(
new WPFunctions(),
$entityManager
);
$notice->init(true);
$notice->init(true);
$notice->init(true);
}
public function testItDoesntDisplayWhenDisabled() {
$this->notice->disable();
$result = $this->notice->init(true);
verify($result)->null();
}
}

View File

@ -86,13 +86,6 @@
'bounceEmail': __('Bounce email address'),
'yourBouncedEmails': __('Your bounced emails will be sent to this address.'),
'readMore': _x('Read more.', 'support article link label'),
'taskCron': __('Newsletter task scheduler (cron)'),
'taskCronDescription': __('Select what will activate your newsletter queue.'),
'websiteVisitors': __('Visitors to your website'),
'serverCron': __("Server side cron (Linux cron)"),
'actionSchedulerCron': __("WordPress built-in cron (recommended)"),
'addCommandToCrontab': __("To use this option please add this command to your crontab:"),
'withFrequency': __("With the frequency of running it every minute:"),
'rolesTitle': __('Roles and capabilities'),
'rolesDescription': __('Manage which WordPress roles access which features of MailPoet.'),
'manageUsingMembers': __('Manage using the Members plugin'),