Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
b602edae52 | |||
7ebcf324a6 | |||
029b698b56 | |||
cbc2be2368 | |||
95476418fa | |||
db354f03c2 | |||
60c18e9cfa | |||
c75bcbd1ab | |||
a91fd0abf1 | |||
7068a7e6b1 | |||
a94efb5563 | |||
7756827b6e | |||
82c0b186d4 | |||
f960b55acb | |||
d137b55dd4 | |||
c039b220a7 | |||
e9969b64ae | |||
12cdba005c | |||
3c9cde6a45 | |||
64c75fd0ca | |||
dd685f3284 | |||
b77cd11c02 | |||
0f4bf52ca2 | |||
8a506e7278 | |||
ee1043fce9 | |||
c987733fba | |||
912cd7965a | |||
f584a673cb | |||
4b69005900 | |||
279364cf86 | |||
53919dd71b | |||
ad46417ee8 | |||
a492658d48 | |||
74b9d3788f | |||
6b3592c4be | |||
6b5fb2e92d | |||
50a00789b8 | |||
ecf0e1d2db | |||
0761998eba | |||
54043e5364 |
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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',
|
||||
|
@ -38,7 +38,7 @@ const sections: Record<string, Section> = {
|
||||
},
|
||||
},
|
||||
customQuery: {
|
||||
order: 'asc',
|
||||
order: 'desc',
|
||||
order_by: 'updated_at',
|
||||
limit: 25,
|
||||
page: 1,
|
||||
|
@ -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) &&
|
||||
|
3
mailpoet/assets/js/src/global.d.ts
vendored
3
mailpoet/assets/js/src/global.d.ts
vendored
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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'],
|
||||
|
@ -49,6 +49,7 @@ export type Settings = {
|
||||
};
|
||||
tracking: {
|
||||
level: 'full' | 'basic' | 'partial';
|
||||
opens: 'merged' | 'separated';
|
||||
};
|
||||
'3rd_party_libs': {
|
||||
enabled: '' | '1';
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
110
mailpoet/composer.lock
generated
@ -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",
|
||||
|
@ -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();
|
||||
|
@ -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 ' .
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 = "
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
113
mailpoet/lib/Util/Notices/DatabaseEngineNotice.php
Normal file
113
mailpoet/lib/Util/Notices/DatabaseEngineNotice.php
Normal 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 plugin’s 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
-
|
||||
|
@ -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
|
||||
|
||||
-
|
||||
|
@ -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']);
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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 plugin’s 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();
|
||||
}
|
||||
}
|
@ -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'),
|
||||
|
Reference in New Issue
Block a user