Compare commits

...

104 Commits

Author SHA1 Message Date
ad6753eb05 Release 4.43.1 2024-02-12 09:41:28 -06:00
51a14a45d3 Use refreshAll() for updates, refresh subscribers/subscriber custom fields conditionally
[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
014b8249bb Replace entityManager->clear with a helper, add cleanup to WP user sync
[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
857ed69c61 Update integration test after change in \MailPoet\Segments\WP
In a previous commit, \MailPoet\Segments\WP was refactored to use
Doctrine instead of Paris. Paris would always convert subscriber emails
to lowercase. We don't do this with the Subscriber entity so it was
necessary to update this test after the refactor.

[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
f0434d74e7 Ensure Doctrine entity cache is cleared after changing the database
ImportExportRepository::updateMultiple() changes subscribers by running
MySQL queries directly. Now that \MailPoet\Segments\WP uses Doctrine as
well this was causing a bug caught by our integration tests.

```
MailPoet\Subscribers\ImportExport\Import\ImportTest::testItSynchronizesWpUsers
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'mary'
+'Mary'

/wp-core/wp-content/plugins/mailpoet/tests/integration/Subscribers/ImportExport/Import/ImportTest.php:719
```

https://app.circleci.com/pipelines/github/mailpoet/mailpoet/16386/workflows/c3fa0cf4-a77d-41ab-a5cc-78d4b37d9228/jobs/278066/tests#failed-test-0

This test was failing because the Doctrine entities were not updated
after the import process ran and modified the subscribers directly in
the database. Running EntityManager::clear() after importing the
subscribers, forces Doctrine to query the database again to update the
entities and prevents this bug.

[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
8318246e1e Improve the way WP users are deleted when running the tests
Doing this to be able to write tests that use any username or email as
it was done in the previous commit. Before the users needed to match the
pattern used inside WPTest::insertUser().

[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
6f98634b94 Replace Subscriber and SubscriberSegment models with Doctrine in \MailPoet\Segments\WP
[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
01cafdf719 Replace Idiomr\ORM with Doctrine in WPTest
[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
374fbe6867 Replace Segment model with Doctrine in \MailPoet\Segments\WP
[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
b63834b02b Change return type as the method always returns a segment
[MAILPOET-5752]
2024-02-12 15:33:28 +01:00
0229eb76ef Only show tutorial icon on the editor
MAILPOET-5735
2024-02-12 13:16:11 +01:00
f8d99b34bf Check table exists before altering it
[MAILPOET-5896]
2024-02-12 11:50:48 +01:00
9f24247285 Add check if meta option is array
[MAILPOET-5891]
2024-02-12 11:15:36 +01:00
db2e61e987 Use "MAX(subscriber_id)" when fething "updated_at" to leverage DB indexes
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
60f1234b71 Check newsletter stats "sentAt" agains correct date (from subsriber task)
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
9db41a5210 Use scheduled task subsriber date for backfilling "sentAt" values
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
7814dda708 Backfill newsletter stats with processed subscribers even with failed status
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
0d71d96cfa Backfill missing sentAt from https://github.com/mailpoet/mailpoet/pull/5416
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
a5d44ba9b6 Backfill missing newsletter statistics from https://github.com/mailpoet/mailpoet/pull/5416
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
78732f8a5d Update also sending queue counts when marking stuck newsletters as sent
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
2804125827 Update also "sent_at" when marking stuck newsletters as sent
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
74f95b972f Split migration code into private methods
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
e3214e441b Add migration to fix sending tasks stuck in "invalid" state
[MAILPOET-5887]
2024-02-12 08:52:30 +02:00
bbe471e653 Use standard formatting for env.sample
MAILPOET-4237
2024-02-08 11:13:35 -06:00
ecd7614337 Add defaults for unset env variables
These defaults maintain backwards compatibility if developers don't
change anything

MAILPOET-4237
2024-02-08 11:13:35 -06:00
a78a31f110 Fix indentation
MAILPOET-4237
2024-02-08 11:13:35 -06:00
fcc4be330c Add set -e to hook scripts to exit on fail
MAILPOET-4237
2024-02-08 11:13:35 -06:00
52397951d8 Rename scripts and make messages consistent
MAILPOET-4237
2024-02-08 11:13:35 -06:00
97f811390d Make install js/php commands optional
MAILPOET-4237
2024-02-08 11:13:35 -06:00
7b64c47733 Don't run any git hooks unless env variable is set
MAILPOET-4237
2024-02-08 11:13:35 -06:00
bc4014d4a3 Make lint-staged commands configurable
MAILPOET-4237
2024-02-08 11:13:35 -06:00
ec6d26162b Add hooks to install updates automatically
MAILPOET-4237
2024-02-08 11:13:35 -06:00
5d8d7adc08 Add command to install just PHP dependencies
MAILPOET-4237
2024-02-08 11:13:35 -06:00
15b351b3f1 Bump follow-redirects from 1.15.2 to 1.15.4
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-08 12:15:20 +01:00
d4d4db7cc5 Release 4.43.0 2024-02-06 14:47:57 +01:00
c2c9429706 Fix TransactionalEmailHooksTest failing on MySQL 8
[MAILPOET-5886]
2024-02-05 15:24:26 +01:00
de9c03a3bc Add migration for fixing status for newsletters stuck as sending
[MAILPOET-5886]
2024-02-05 15:24:26 +01:00
7b86de1346 Set sentAt date when saving newsletter stats
MySQL 8 can't handle null values for not null columns even when we set a default
See detailed explanation
https://kedar.nitty-witty.com/blog/mysql-8-timestamp-cannot-be-null-and-explicit_defaults_for_timestamp
[MAILPOET-5886]
2024-02-05 15:24:26 +01:00
749c4e5e43 Prevent deleting and further changes in detached tasks during sending
When a post notification history is deleted in
MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter::preProcessNewsletter
it is also detached from EntityManager.

Any further attempt to manipulate the entity via EntityManager (remove or flush) causes
errors like "ERROR: A new entity was found through the relationship 'MailPoet\Entities\ScheduledTaskEntity#sendingQueue'"

In this commit we prevent doing such changes.
[MAILPOET-5880]
2024-02-05 15:16:11 +01:00
6a86dfc7c0 Throw an exception when preprocessing a newsletter without a queue
The goal of this commit is to change the preProcessNewsletter method
to return false only in case when it deleted the newsletter and
all associated entities. So that we know for sure that false means all was deleted.
[MAILPOET-5880]
2024-02-05 15:16:11 +01:00
ac6cc881d1 Fix error when permanently deleting a legacy automation
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
7bfcf4bd67 Move redirect to useEffect
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
f24184fe16 Add automation name also to URL-based notices
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
6994afebb7 Remove unused URL-based notices
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
bf8e0f344a Unify automation listing notices, include automation name
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
f4157947c4 Fix and improve automation listing table footer and pagination styles
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
2665ca65ae Use real number of automations for table placeholder
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
bf70c24511 Remove no longer needed welcome email setup code
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
e3a194a1dd Remove no longer needed notices (it redirects to automations)
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
c388c2ae9b Fix not waiting until a template is saved (and causing error in FF)
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
646d8dbea4 Fix updating legacy automation status on trash, restore, and delete
[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
4b3744717f Revert "Unify responses of newsletter editing API methods with get()"
This reverts commit e0b6cf7b41.

In the next commit, I will add a different fix for this issue.

[MAILPOET-5833]
2024-02-05 13:28:15 +01:00
5824905f18 Independently update existing subscribers info and status in import
[MAILPOET-5617]
2024-02-05 12:54:02 +01:00
de1fc6601c Always display small sender notices
in the places where we always rewrite the emails (Automations).

[MAILPOET-5844]
2024-02-05 10:52:56 +01:00
d65fdd7a6d Reset currentRecords only when needed
And reloadCache after domain verification.

[MAILPOET-5844]
2024-02-05 10:52:56 +01:00
928c100839 Rename constant
[MAILPOET-5844]
2024-02-05 10:52:56 +01:00
1e373ab7e0 Update Sender Domain inline notice
Remove unused notices displayed before enforcement date.

[MAILPOET-5844]
2024-02-05 10:52:56 +01:00
01afa2ef31 Update Sender Domain page banners
Remove unused notices displayed before enforcement date.

[MAILPOET-5844]
2024-02-05 10:52:56 +01:00
3f8c0f2bc6 Remove restrictionsApply function
[MAILPOET-5844]
2024-02-05 10:52:56 +01:00
bd75f74cb5 Remove Enable Automations notice
[MAILPOET-5883]
2024-02-05 08:56:34 +02:00
41dff5f95a Move tabConfig so translations work
[MAILPOET-5852]
2024-02-02 13:16:44 +01:00
b3bd371a27 Make domain authentication notices translateable
[MAILPOET-5852]
2024-02-02 13:16:44 +01:00
f09ddaff1e Disable how-to instructions under the FormTokenField
These strings are not translateable for us at the moment. Therefore I disabled them

[MAILPOET-5852]
2024-02-02 13:16:44 +01:00
4a96f3dfea Make shortcode text translateable
[MAILPOET-5852]
2024-02-02 13:16:44 +01:00
bc7fbd2469 Extract shared detachAll/refreshAll logic to a method
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
8450e1bb1b Clear entity manager for every cron run
This avoids using stale data and prevents memory leaks.

[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
2b5135474e Save deleted newsletter in woocommerce purchase stats as NULL
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
877c736190 Add a missing detachAll() helper to a batch delete
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
a947dd36cd Use refreshAll() helper for batch updates
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
d6da2af55b Extract newsletter bulk delete logic to a controller
Repositories shouldn't inject other repositories. Also, this solves circular DI dependency.

[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
82d5621caf Extract newsletter entity deletion to a method
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
cb803d6f4b Use epxlicit transaction demarcation
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
082ab78f90 Remove no longer needed cleanup
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
d1f48b6772 Detach deleted newsletters, simplify code
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
f5e1ae9fba Avoid unnecessary JOIN
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
4454dd6203 Delete stats notifications and tasks using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
ddfde2059e Delete newsletter data from woocommerce purchase stats using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
682527cd1b Add a helper method to refresh all (or some) entities of a given class
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
aa13c8956d Delete newsletter segments using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
18b227429f Delete sending queues using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
98d46f2fdb Delete scheduled tasks using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
bcc96d27bb Delete scheduled tasks subscribers using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
9aaffd9ceb Delete newsletter links using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
115616620e Delete newsletter options using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
bca9101921 Delete newsletter posts using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
1221a64d3d Delete statistics clicks using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
17168643c9 Delete statistics opens using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
c492ff4d8d Delete statistics newsletters using a repository
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
6f844e2bea Use typed properties
[MAILPOET-5845]
2024-02-02 12:48:40 +01:00
5646464853 Refactor Mailer::formatSubscriberNameAndEmailAddress() to use Doctrine
[MAILPOET-5758]
2024-01-31 12:44:58 +01:00
42b5efda1b Remove Tasks\Sending
This class is not used anymore and can be removed.

[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
a1017c4380 Remove last use of the Tasks\Sending class
I was not able to figure out a easy replacement for this use. As far as
I could check, Newsletter::getQueue() is only called inside
Newsletter::withSendingQueue() and the later is not used anywhere. That
is why I opeted to simply remove the call to
SendingTask::getByNewsletterId().

This could break third-party code using Newsletter::getQueue(). At least they
will get a deprecation notice and won't get a fatal error.

[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
0409747216 Remove outdated code comment
This class doesn't use MailPoet\Tasks\Sending anymore.

[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
4ab07621b3 Refactor API\NewsletterTest to use Doctrine instead of Sending
[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
881a971b8e Refactor Models\NewsletterTest to use Doctrine instead of Sending
[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
5b913f7fe8 Refactor UnsubscribesTest to use Doctrine instead of Sending
[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
756160abc5 Refactor ViewInBrowserRendererTest to use Doctrine instead of Sending
[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
ee04736a88 Refactor ViewInBrowserControllerTest to use Doctrine instead of Sending
[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
6f5e22c54c Refactor WelcomeTest to use Doctrine instead of Sending
[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
7c2065bcbf Refactor NewsletterRepositoryTest to use Doctrine instead of Sending
[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
19b33e15f5 Refactor SendingQueue::add() to use Doctrine instead of Tasks\Sending
[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
62db03d95b Update findOneByNewsletterAndTaskStatus() to work with null status
This commits updates the method findOneByNewsletterAndTaskStatus() so
that it can retrieve SendingQueues when the task status is null.

[MAILPOET-5737]
2024-01-31 12:21:54 +01:00
8734d9762b Expand intregration test coverage for API/JSON/v1/SendingQueue.php
Doing this before refactoring this class to use Doctrine instead of
Paris.

[MAILPOET-5684]
2024-01-31 12:21:54 +01:00
4d021eeceb Release 4.42.1 2024-01-30 15:34:51 +01:00
116 changed files with 2871 additions and 2199 deletions

42
.husky/common.sh Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
. "$(dirname "$0")/../mailpoet/.env"
export MP_GIT_HOOKS_ENABLE="${MP_GIT_HOOKS_ENABLE:-true}"
export MP_GIT_HOOKS_ESLINT="${MP_GIT_HOOKS_ESLINT:-true}"
export MP_GIT_HOOKS_STYLELINT="${MP_GIT_HOOKS_STYLELINT:-true}"
export MP_GIT_HOOKS_PHPLINT="${MP_GIT_HOOKS_PHPLINT:-true}"
export MP_GIT_HOOKS_CODE_SNIFFER="${MP_GIT_HOOKS_CODE_SNIFFER:-true}"
export MP_GIT_HOOKS_MINIMAL_PLUGIN_STANDARDS="${MP_GIT_HOOKS_MINIMAL_PLUGIN_STANDARDS:-true}"
export MP_GIT_HOOKS_PHPSTAN="${MP_GIT_HOOKS_PHPSTAN:-true}"
export MP_GIT_HOOKS_INSTALL_JS="${MP_GIT_HOOKS_INSTALL_JS:-false}"
export MP_GIT_HOOKS_INSTALL_PHP="${MP_GIT_HOOKS_INSTALL_PHP:-false}"
fileChanged() {
local filePattern="$1"
local changedFiles="$2"
if echo "$changedFiles" | grep -qE "$filePattern"; then
return 0
else
return 1
fi
}
installIfUpdates() {
local changedFiles="$(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD)"
if [ "$MP_GIT_HOOKS_INSTALL_JS" = "true" ] && fileChanged "pnpm-lock.yaml" "$changedFiles"; then
echo "Change detected in pnpm-lock.yaml, running do install:js"
pushd mailpoet
./do install:js
popd
fi
if [ "$MP_GIT_HOOKS_INSTALL_PHP" = "true" ] && fileChanged "mailpoet/composer.lock" "$changedFiles"; then
echo "Change detected in mailpoet/composer.lock, running do install:php"
pushd mailpoet
./do install:php
popd
fi
}

6
.husky/post-checkout Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ "$MP_GIT_HOOKS_ENABLE" != "true" ] && exit 0
installIfUpdates

7
.husky/post-merge Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ "$MP_GIT_HOOKS_ENABLE" != "true" ] && exit 0
installIfUpdates

7
.husky/post-rewrite Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ "$MP_GIT_HOOKS_ENABLE" != "true" ] && exit 0
installIfUpdates

View File

@ -1,5 +1,7 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
[ "$MP_GIT_HOOKS_ENABLE" != "true" ] && exit 0
npx lint-staged -c mailpoet/package.json --cwd mailpoet
npx lint-staged -c package.json

View File

@ -44,3 +44,14 @@ WP_TEST_PERFORMANCE_DATA_URL=
WP_TEST_PERFORMANCE_PW=
K6_CLOUD_TOKEN=
K6_CLOUD_ID=
# git hooks configuration (must be 'true' to enable)
MP_GIT_HOOKS_ENABLE=true
MP_GIT_HOOKS_ESLINT=true
MP_GIT_HOOKS_STYLELINT=true
MP_GIT_HOOKS_PHPLINT=true
MP_GIT_HOOKS_CODE_SNIFFER=true
MP_GIT_HOOKS_MINIMAL_PLUGIN_STANDARDS=true
MP_GIT_HOOKS_PHPSTAN=true
MP_GIT_HOOKS_INSTALL_JS=false
MP_GIT_HOOKS_INSTALL_PHP=false

View File

@ -28,6 +28,14 @@ class RoboFile extends \Robo\Tasks {
->run();
}
public function installPhp() {
return $this->taskExecStack()
->stopOnFail()
->exec('./tools/vendor/composer.phar install')
->addCode([$this, 'cleanupCachedFiles'])
->run();
}
public function installJs() {
return $this->taskExecStack()
->stopOnFail()

View File

@ -12,9 +12,13 @@
}
}
.mailpoet-automation-listing {
.mailpoet-automation-listing.woocommerce-table {
box-shadow: none;
margin-bottom: 0;
.woocommerce-pagination {
margin-bottom: 0;
}
}
.mailpoet-automation-listing-cell-name.woocommerce-table__item {

View File

@ -31,6 +31,7 @@ export function FormTokenField({
suggestions={suggestions.map((item) => item.name)}
__experimentalExpandOnFocus
__experimentalAutoSelectFirstMatch
__experimentalShowHowTo={false}
placeholder={placeholder}
onChange={(raw: string[]) => {
const allSelected: FormTokenItem[] = raw

View File

@ -115,21 +115,25 @@ function Editor(): JSX.Element {
if (!isBooting) {
return;
}
if (automation.status === 'trash') {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
notice: LISTING_NOTICES.automationHadBeenDeleted,
'notice-args': [automation.name],
});
}
updatingActiveAutomationNotPossible();
setIsBooting(false);
}, [isBooting]);
}, [automation.name, automation.status, isBooting]);
if (automation.status === 'trash') {
return null;
}
const className = classnames('interface-interface-skeleton', {
'is-sidebar-opened': isSidebarOpened,
'show-icon-labels': showIconLabels,
});
if (automation.status === 'trash') {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
notice: LISTING_NOTICES.automationHadBeenDeleted,
});
return null;
}
return (
<ShortcutProvider>
<SlotFillProvider>

View File

@ -214,9 +214,10 @@ export function* trash(onTrashed: () => void = undefined) {
onTrashed?.();
if (data?.status === AutomationStatus.TRASH) {
if (data?.data?.status === AutomationStatus.TRASH) {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
notice: LISTING_NOTICES.automationDeleted,
'notice-args': [automation.name],
});
}

View File

@ -3,13 +3,12 @@ import { __ } from '@wordpress/i18n';
export function ShortcodeHelpText(): JSX.Element {
return (
<span className="mailpoet-shortcode-selector">
You can use{' '}
<a
href="https://kb.mailpoet.com/article/215-personalize-newsletter-with-shortcodes"
target="_blank"
rel="noopener noreferrer"
>
{__('MailPoet shortcodes', 'mailpoet')}
{__('You can use MailPoet shortcodes.', 'mailpoet')}
</a>
</span>
);

View File

@ -45,6 +45,7 @@ export function ListPanel(): JSX.Element {
values.map((item) => item.id),
);
}}
__experimentalShowHowTo={false}
/>
</PanelBody>
);

View File

@ -63,6 +63,7 @@ export function RolePanel(): JSX.Element {
items.map((item) => item.id),
);
}}
__experimentalShowHowTo={false}
/>
</PanelBody>
);

View File

@ -1,12 +1,10 @@
import { useEffect } from 'react';
import { dispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { getQueryArg, removeQueryArgs } from '@wordpress/url';
export const LISTING_NOTICES = {
automationActivated: 'activated',
automationSaved: 'saved',
automationDeleted: 'deleted',
automationHadBeenDeleted: 'had-been-deleted',
} as const;
@ -16,29 +14,34 @@ export function useAutomationListingNotices(): void {
useEffect(() => {
const notice = getQueryArg(window.location.href, 'notice');
if (notice === LISTING_NOTICES.automationActivated) {
createNotice('success', __('Your automation is now active.', 'mailpoet'));
} else if (notice === LISTING_NOTICES.automationSaved) {
const args = getQueryArg(window.location.href, 'notice-args') ?? [];
const automationName = args[0] ?? 'Unknown';
if (notice === LISTING_NOTICES.automationDeleted) {
createNotice(
'success',
__('Your automation has been saved.', 'mailpoet'),
);
} else if (notice === LISTING_NOTICES.automationDeleted) {
createNotice(
'success',
__('1 automation moved to the Trash.', 'mailpoet'),
sprintf(
__('Automation "%s" was moved to the trash.', 'mailpoet'),
automationName,
),
);
} else if (notice === LISTING_NOTICES.automationHadBeenDeleted) {
createNotice(
'error',
__(
'You cannot edit this automation because it is in the Trash.',
'mailpoet',
sprintf(
__(
'You cannot edit automation "%s" because it is in the trash.',
'mailpoet',
),
automationName,
),
);
}
const urlWithoutNotices = removeQueryArgs(window.location.href, 'notice');
const urlWithoutNotices = removeQueryArgs(
window.location.href,
'notice',
'notice-args',
);
window.history.replaceState('', '', urlWithoutNotices);
}, [createNotice]);
}

View File

@ -14,32 +14,10 @@ import { plusIcon } from 'common/button/icon/plus';
import { getRow } from './get-row';
import { AutomationItem, storeName } from './store';
import { Automation, AutomationStatus } from './automation';
import { automationCount, legacyAutomationCount } from '../config';
import { MailPoet } from '../../mailpoet';
import { PageHeader } from '../../common/page-header';
const tabConfig = [
{
name: 'all',
title: __('All', 'mailpoet'),
className: 'mailpoet-tab-all',
},
{
name: AutomationStatus.ACTIVE,
title: __('Active', 'mailpoet'),
className: 'mailpoet-tab-active',
},
{
name: AutomationStatus.DRAFT,
title: _x('Draft', 'noun', 'mailpoet'),
className: 'mailpoet-tab-draft',
},
{
name: AutomationStatus.TRASH,
title: _x('Trash', 'noun', 'mailpoet'),
className: 'mailpoet-tab-trash',
},
] as const;
const tableHeaders = [
{
key: 'name',
@ -126,26 +104,48 @@ export function AutomationListing(): JSX.Element {
return grouped;
}, [automations]);
const tabs = useMemo(
() =>
tabConfig.map((tab) => {
const count = (groupedAutomations[tab.name] ?? []).length;
return {
name: tab.name,
title: (
<>
<span>{tab.title}</span>
{count > 0 && <span className="count">{count}</span>}
</>
) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- typed as string but supports JSX
className: tab.className,
};
}),
[groupedAutomations],
);
const tabs = useMemo(() => {
const tabConfig = [
{
name: 'all',
title: __('All', 'mailpoet'),
className: 'mailpoet-tab-all',
},
{
name: AutomationStatus.ACTIVE,
title: __('Active', 'mailpoet'),
className: 'mailpoet-tab-active',
},
{
name: AutomationStatus.DRAFT,
title: _x('Draft', 'noun', 'mailpoet'),
className: 'mailpoet-tab-draft',
},
{
name: AutomationStatus.TRASH,
title: _x('Trash', 'noun', 'mailpoet'),
className: 'mailpoet-tab-trash',
},
] as const;
return tabConfig.map((tab) => {
const count = (groupedAutomations[tab.name] ?? []).length;
return {
name: tab.name,
title: (
<>
<span>{tab.title}</span>
{count > 0 && <span className="count">{count}</span>}
</>
) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- typed as string but supports JSX
className: tab.className,
};
});
}, [groupedAutomations]);
const renderTabs = useCallback(
(tab) => {
const totalCount = automationCount + legacyAutomationCount;
const filteredAutomations: AutomationItem[] =
groupedAutomations[tab.name] ?? [];
const rowsPerPage = parseInt(pageSearch.get('per_page') ?? '25', 10);
@ -171,11 +171,11 @@ export function AutomationListing(): JSX.Element {
filteredAutomations[i].id *
(filteredAutomations[i].isLegacy ? -1 : 1)
}
rowsPerPage={rowsPerPage}
rowsPerPage={Math.min(rowsPerPage, totalCount)}
onQueryChange={(key) => (value) => {
updateUrlSearchString({ [key]: value });
}}
totalRows={filteredAutomations.length}
totalRows={automations ? filteredAutomations.length : totalCount}
query={Object.fromEntries(pageSearch)}
showMenu={false}
/>

View File

@ -47,7 +47,10 @@ export function* trashAutomation(automation: Automation) {
},
});
const message = __('1 automation moved to the Trash.', 'mailpoet');
const message = sprintf(
__('Automation "%s" was moved to the trash.', 'mailpoet'),
automation.name,
);
void createSuccessNotice(message, {
id: `automation-trashed-${automation.id}`,
__unstableHTML: (
@ -81,7 +84,10 @@ export function* restoreAutomation(
void removeNotice(`automation-trashed-${automation.id}`);
const message = __('1 automation restored from the Trash.', 'mailpoet');
const message = sprintf(
__('Automation "%s" was restored from the trash.', 'mailpoet'),
automation.name,
);
void createSuccessNotice(message, {
__unstableHTML: (
<p>
@ -107,7 +113,13 @@ export function* deleteAutomation(automation: Automation) {
});
void createSuccessNotice(
__('1 automation and all associated data permanently deleted.', 'mailpoet'),
sprintf(
__(
'Automation "%s" and all associated data were permanently deleted.',
'mailpoet',
),
automation.name,
),
);
return {

View File

@ -1,4 +1,4 @@
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { __unstableAwaitPromise as AwaitPromise } from '@wordpress/data-controls';
import { dispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
@ -56,7 +56,7 @@ export function* loadLegacyAutomations() {
}
export function* trashLegacyAutomation(automation: Automation) {
const data: { data: ListingItem } = yield AwaitPromise(
yield AwaitPromise(
legacyApiFetch({
endpoint: 'newsletters',
method: 'trash',
@ -64,11 +64,17 @@ export function* trashLegacyAutomation(automation: Automation) {
}),
);
createSuccessNotice(__('1 automation moved to the Trash.', 'mailpoet'));
createSuccessNotice(
sprintf(
__('Automation "%s" was moved to the trash.', 'mailpoet'),
automation.name,
),
);
return {
type: 'UPDATE_LEGACY_AUTOMATION',
automation: mapToAutomation(data.data),
type: 'UPDATE_LEGACY_AUTOMATION_STATUS',
id: automation.id,
status: AutomationStatus.TRASH,
} as const;
}
@ -83,16 +89,22 @@ export function* restoreLegacyAutomation(automation: Automation) {
void removeNotice(`automation-trashed-${automation.id}`);
createSuccessNotice(__('1 automation restored from the Trash.', 'mailpoet'));
createSuccessNotice(
sprintf(
__('Automation "%s" was restored from the trash.', 'mailpoet'),
automation.name,
),
);
return {
type: 'UPDATE_LEGACY_AUTOMATION',
automation: mapToAutomation(data.data),
type: 'UPDATE_LEGACY_AUTOMATION_STATUS',
id: automation.id,
status: data.data.status,
} as const;
}
export function* deleteLegacyAutomation(automation: Automation) {
const data: { data: ListingItem } = yield AwaitPromise(
yield AwaitPromise(
legacyApiFetch({
endpoint: 'newsletters',
method: 'delete',
@ -101,11 +113,17 @@ export function* deleteLegacyAutomation(automation: Automation) {
);
void createSuccessNotice(
__('1 automation and all associated data permanently deleted.', 'mailpoet'),
sprintf(
__(
'Automation "%s" and all associated data were permanently deleted.',
'mailpoet',
),
automation.name,
),
);
return {
type: 'DELETE_LEGACY_AUTOMATION',
automation: mapToAutomation(data.data),
id: automation.id,
} as const;
}

View File

@ -78,7 +78,7 @@ const getAutomaticInfo = (item: ListingItem): ReactNode => {
}
const metaOptionValues =
meta && meta.option
meta && meta.option && Array.isArray(meta.option)
? meta.option.map(({ name }: { name: string }) => name)
: [];

View File

@ -8,21 +8,21 @@ export function legacyReducer(state: State, action): State {
...state,
legacyAutomations: action.automations,
};
case 'UPDATE_LEGACY_AUTOMATION':
case 'UPDATE_LEGACY_AUTOMATION_STATUS':
return {
...state,
legacyAutomations: state.legacyAutomations.map(
(automation: Automation) =>
automation.id === action.automation.id
? (action.automation as Automation)
automation.id === action.id
? { ...automation, status: action.status }
: automation,
),
};
case 'DELETE_LEGACY_AUTOMATION':
return {
...state,
legacyAutomations: state.automations.filter(
(automation: Automation) => automation.id !== action.automation.id,
legacyAutomations: state.legacyAutomations.filter(
(automation: Automation) => automation.id !== action.id,
),
};
default:

View File

@ -57,6 +57,7 @@ export function TokenField({
value={selectedValues}
suggestions={suggestedValues}
onChange={(tokens) => handleSave(tokens, onChange)}
__experimentalShowHowTo={false}
/>
);
}

View File

@ -8,8 +8,8 @@ import { SenderActions } from './sender-domain-notice-actions';
export type SenderRestrictionsType = {
lowerLimit: number;
isNewUser: boolean;
isEnforcementOfNewRestrictionsInEffect: boolean;
isAuthorizedDomainRequiredForNewCampaigns?: boolean;
campaignTypes?: string[];
alwaysRewrite?: boolean;
};
@ -56,20 +56,12 @@ function SenderDomainInlineNotice({
const LOWER_LIMIT = senderRestrictions?.lowerLimit || 500;
const isNewUser = senderRestrictions?.isNewUser ?? true;
const isEnforcementOfNewRestrictionsInEffect =
senderRestrictions?.isEnforcementOfNewRestrictionsInEffect ?? true;
// TODO: Remove after the enforcement date has passed
const onlyShowWarnings =
!isNewUser && !isEnforcementOfNewRestrictionsInEffect;
const isSmallSender = subscribersCount <= LOWER_LIMIT;
if (
isSmallSender ||
isPartiallyVerifiedDomain ||
senderRestrictions.alwaysRewrite ||
onlyShowWarnings
senderRestrictions.alwaysRewrite
) {
isAlert = false;
}
@ -102,8 +94,7 @@ function SenderDomainInlineNotice({
emailAddressDomain={emailAddressDomain}
isFreeDomain={isFreeDomain}
isPartiallyVerifiedDomain={isPartiallyVerifiedDomain}
isSmallSender={isSmallSender}
onlyShowWarnings={onlyShowWarnings}
isSmallSender={isSmallSender || senderRestrictions.alwaysRewrite}
/>
</InlineNotice>
);

View File

@ -7,49 +7,45 @@ function SenderDomainNoticeBody({
isFreeDomain,
isPartiallyVerifiedDomain,
isSmallSender,
onlyShowWarnings = false,
alwaysRewrite = false,
}: {
emailAddressDomain: string;
isFreeDomain: boolean;
isPartiallyVerifiedDomain: boolean;
isSmallSender: boolean;
onlyShowWarnings?: boolean;
alwaysRewrite?: boolean;
}) {
const renderMessage = (messageKey: string) => {
const messages: { [key: string]: string } = {
freeSmall:
freeSmall: __(
"Shared 3rd-party domains like <emailDomain/> will send from MailPoet's shared domain. We recommend that you use your site's branded domain instead.",
free: "MailPoet cannot send email campaigns from shared 3rd-party domains like <emailDomain/>. Please send from your site's branded domain instead.",
// TODO: Remove freeWarning after the enforcement date has passed
freeWarning:
"Starting on February 1st, 2024, MailPoet will no longer be able to send from email addresses on shared 3rd party domains like <emailDomain/>. Please send from your site's branded domain instead.",
partiallyVerified:
'mailpoet',
),
free: __(
"MailPoet cannot send email campaigns from shared 3rd-party domains like <emailDomain/>. Please send from your site's branded domain instead.",
'mailpoet',
),
partiallyVerified: __(
'Update your domain settings to improve email deliverability and meet new sending requirements.',
smallSender:
'mailpoet',
),
smallSender: __(
'Authenticate to send as <emailDomain/> and improve email deliverability.',
default: 'Authenticate domain to send new emails as <emailDomain/>.',
'mailpoet',
),
default: __(
'Authenticate domain to send new emails as <emailDomain/>.',
'mailpoet',
),
};
const defaultMessage = messages[messageKey] || messages.default;
return createInterpolateElement(__(defaultMessage, 'mailpoet'), {
return createInterpolateElement(defaultMessage, {
emailDomain: <strong>{escapeHTML(emailAddressDomain)}</strong>,
});
};
// TODO: Remove after the enforcement date has passed
if (onlyShowWarnings) {
if (isFreeDomain) {
return renderMessage(isSmallSender ? 'freeSmall' : 'freeWarning');
}
if (isPartiallyVerifiedDomain) {
return renderMessage('partiallyVerified');
}
return renderMessage('smallSender');
}
if (isFreeDomain) {
return renderMessage(isSmallSender || alwaysRewrite ? 'freeSmall' : 'free');
}

View File

@ -155,9 +155,6 @@ interface Window {
mailpoet_partially_verified_sender_domains?: string[];
mailpoet_sender_restrictions?: {
lowerLimit: number;
upperLimit: number;
isNewUser: boolean;
isEnforcementOfNewRestrictionsInEffect: boolean;
isAuthorizedDomainRequiredForNewCampaigns?: boolean;
campaignTypes?: string[];
};

View File

@ -1,11 +1,9 @@
import { MailPoet } from 'mailpoet';
import { Button } from 'common/button/button';
import { AutomationsInfoNotice } from 'notices/automations-info-notice';
export function KnowledgeBase() {
return (
<>
<AutomationsInfoNotice />
<p>{MailPoet.I18n.t('knowledgeBaseIntro')}</p>
<ul className="mailpoet-text-links">
<li>

View File

@ -119,9 +119,8 @@ const stepsListingHeading = (
{' '}
</h1>
<div className="mailpoet-flex-grow" />
{!['automation', 'automation_transactional'].includes(emailType) && (
<TutorialIcon />
)}
{!['automation', 'automation_transactional'].includes(emailType) &&
step === 3 && <TutorialIcon />}
</div>
);
};

View File

@ -16,7 +16,6 @@ import { NewsletterSend } from 'newsletters/send';
import { Congratulate } from 'newsletters/send/congratulate/congratulate.jsx';
import { NewsletterTypeStandard } from 'newsletters/types/standard.jsx';
import { NewsletterNotification } from 'newsletters/types/notification/notification.jsx';
import { NewsletterWelcome } from 'newsletters/types/welcome/welcome.jsx';
import { NewsletterTypeReEngagement } from 'newsletters/types/re-engagement/re-engagement';
import { NewsletterListStandard } from 'newsletters/listings/standard.jsx';
import { NewsletterListNotification } from 'newsletters/listings/notification.jsx';
@ -131,7 +130,7 @@ const routes = [
render: withBoundary(Tabs),
},
{
path: '/(standard|welcome|notification|re_engagement)/(.*)?',
path: '/(standard|notification|re_engagement)/(.*)?',
render: withBoundary(Tabs),
},
/* New newsletter: types */
@ -143,10 +142,6 @@ const routes = [
path: '/new/notification',
render: withBoundary(NewsletterNotification),
},
{
path: '/new/welcome',
render: withBoundary(NewsletterWelcome),
},
{
path: '/new/re-engagement',
render: withBoundary(NewsletterTypeReEngagement),

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { ChangeEvent, Component, ContextType } from 'react';
import jQuery from 'jquery';
import { __, _x, sprintf } from '@wordpress/i18n';
import { __, _x } from '@wordpress/i18n';
import { History, Location } from 'history';
import ReactStringReplace from 'react-string-replace';
import slugify from 'slugify';
@ -349,12 +349,13 @@ class NewsletterSendComponent extends Component<
body: JSON.stringify(response.data.body),
categories: '["recent"]',
},
}).fail((err) => {
this.showError(err);
this.setState({ loading: false });
MailPoet.Modal.loading(false);
});
done();
})
.then(() => done())
.fail((err) => {
this.showError(err);
this.setState({ loading: false });
MailPoet.Modal.loading(false);
});
})
.catch((err) => {
this.showError({ errors: [err] });
@ -512,22 +513,11 @@ class NewsletterSendComponent extends Component<
this.state.item.type === 'automatic' &&
automaticEmails[opts.group]
) {
this.context.notices.success(
<p>
{sprintf(
__('Your %1s Automatic Email is now activated!', 'mailpoet'),
automaticEmails[opts.group]?.title ?? '',
)}
</p>,
);
MailPoet.trackEvent('Emails > Automatic email activated', {
Type: slugify(`${opts.group}-${opts.event}`),
Delay: getTimingValueForTracking(opts),
});
} else if (response.data.type === 'welcome') {
this.context.notices.success(
<p>{__('Your Welcome Email is now activated!', 'mailpoet')}</p>,
);
MailPoet.trackEvent('Emails > Welcome email activated', {
'List type': opts.event,
Delay: getTimingValueForTracking(opts),

View File

@ -1,130 +0,0 @@
import { Component } from 'react';
import _ from 'underscore';
import jQuery from 'jquery';
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
import { Background } from 'common/background/background';
import { Button } from 'common/button/button';
import { Heading } from 'common/typography/heading/heading';
import { Grid } from 'common/grid';
import { ListingHeadingStepsRoute } from 'newsletters/listings/heading-steps-route';
import { MailPoet } from 'mailpoet';
import { WelcomeScheduling } from './scheduling.jsx';
const field = {
name: 'options',
label: 'Event',
type: 'reactComponent',
component: WelcomeScheduling,
};
class NewsletterWelcome extends Component {
constructor(props) {
super(props);
let availableSegments = window.mailpoet_segments || [];
let defaultSegment = 1;
availableSegments = availableSegments.filter(
(segment) => segment.type === 'default',
);
if (_.size(availableSegments) > 0) {
defaultSegment = _.first(availableSegments).id;
}
this.state = {
options: {
event: 'segment',
segment: defaultSegment,
role: 'subscriber',
afterTimeNumber: 1,
afterTimeType: 'immediate',
},
};
this.handleValueChange = this.handleValueChange.bind(this);
this.handleNext = this.handleNext.bind(this);
}
handleValueChange(event) {
const { state } = this;
state[event.target.name] = event.target.value;
this.setState(state);
}
handleNext(event) {
event.preventDefault();
if (!this.isValid()) {
this.validate();
return;
}
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'create',
data: _.extend({}, this.state, {
type: 'welcome',
subject: __('Subject', 'mailpoet'),
}),
})
.done((response) => {
this.showTemplateSelection(response.data.id);
})
.fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map((error) => error.message),
{ scroll: true },
);
}
});
}
isValid = () => jQuery('#welcome_scheduling').parsley().isValid();
validate = () => jQuery('#welcome_scheduling').parsley().validate();
showTemplateSelection(newsletterId) {
this.props.history.push(`/template/${newsletterId}`);
}
render() {
return (
<div>
<Background color="#fff" />
<ListingHeadingStepsRoute
emailType="welcome"
automationId="welcome_email_creation_heading"
/>
<Grid.Column align="center" className="mailpoet-schedule-email">
<Heading level={4}>
{__('When to send this welcome email?', 'mailpoet')}
</Heading>
<form id="welcome_scheduling">
<WelcomeScheduling
item={this.state}
field={field}
onValueChange={this.handleValueChange}
/>
<Button isFullWidth type="submit" onClick={this.handleNext}>
{__('Next', 'mailpoet')}
</Button>
</form>
</Grid.Column>
</div>
);
}
}
NewsletterWelcome.propTypes = {
history: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
};
NewsletterWelcome.displayName = 'NewsletterWelcome';
export { NewsletterWelcome };

View File

@ -1,34 +0,0 @@
import { createInterpolateElement } from '@wordpress/element';
import { MailPoet } from 'mailpoet';
import { Notice } from 'notices/notice';
function AutomationsInfoNotice() {
const automationsInfo = createInterpolateElement(
MailPoet.I18n.t('automationsInfoNotice'),
{
link1: (
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
<a
rel="noreferrer"
href="https://kb.mailpoet.com/article/397-how-to-set-up-an-automation"
target="_blank"
/>
),
link2: (
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
<a
rel="noreferrer"
href="https://kb.mailpoet.com/article/408-integration-with-automatewoo"
target="_blank"
/>
),
},
);
return (
<Notice type="warning" scroll renderInPlace timeout={false}>
<p>{automationsInfo}</p>
</Notice>
);
}
export { AutomationsInfoNotice };

View File

@ -1,19 +1,15 @@
import { SaveButton } from 'settings/components';
import { AutomationsInfoNotice } from 'notices/automations-info-notice';
import { EmailCustomizer } from './email-customizer';
import { CheckoutOptin } from './checkout-optin';
import { SubscribeOldCustomers } from './subscribe-old-customers';
export function WooCommerce() {
return (
<>
<AutomationsInfoNotice />
<div className="mailpoet-settings-grid">
<EmailCustomizer />
<CheckoutOptin />
<SubscribeOldCustomers />
<SaveButton />
</div>
</>
<div className="mailpoet-settings-grid">
<EmailCustomizer />
<CheckoutOptin />
<SubscribeOldCustomers />
<SaveButton />
</div>
);
}

View File

@ -243,8 +243,6 @@ class NewslettersResponseBuilder {
if ($task === null) {
return null;
}
// the following crazy mix of '$queue' and '$task' comes from 'array_merge($task, $queue)'
// (MailPoet\Tasks\Sending) which means all equal-named fields will be taken from '$queue'
return [
'id' => (string)$queue->getId(), // (string) for BC
'type' => $task->getType(),

View File

@ -15,6 +15,7 @@ use MailPoet\Entities\SendingQueueEntity;
use MailPoet\InvalidStateException;
use MailPoet\Listing;
use MailPoet\Newsletter\Listing\NewsletterListingRepository;
use MailPoet\Newsletter\NewsletterDeleteController;
use MailPoet\Newsletter\NewsletterSaveController;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\NewsletterValidator;
@ -73,6 +74,8 @@ class Newsletters extends APIEndpoint {
/** @var NewsletterSaveController */
private $newsletterSaveController;
private NewsletterDeleteController $newsletterDeleteController;
/** @var NewsletterUrl */
private $newsletterUrl;
@ -98,6 +101,7 @@ class Newsletters extends APIEndpoint {
Emoji $emoji,
SendPreviewController $sendPreviewController,
NewsletterSaveController $newsletterSaveController,
NewsletterDeleteController $newsletterDeleteController,
NewsletterUrl $newsletterUrl,
Scheduler $scheduler,
NewsletterValidator $newsletterValidator,
@ -115,6 +119,7 @@ class Newsletters extends APIEndpoint {
$this->emoji = $emoji;
$this->sendPreviewController = $sendPreviewController;
$this->newsletterSaveController = $newsletterSaveController;
$this->newsletterDeleteController = $newsletterDeleteController;
$this->newsletterUrl = $newsletterUrl;
$this->scheduler = $scheduler;
$this->newsletterValidator = $newsletterValidator;
@ -163,8 +168,6 @@ class Newsletters extends APIEndpoint {
$newsletter = $this->newsletterSaveController->save($data);
$response = $this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
]);
$previewUrl = $this->getViewInBrowserUrl($newsletter);
$response = $this->wp->applyFilters('mailpoet_api_newsletters_save_after', $response);
@ -239,11 +242,7 @@ class Newsletters extends APIEndpoint {
$this->newslettersRepository->flush();
return $this->successResponse(
$this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
])
$this->newslettersResponseBuilder->build($newsletter)
);
}
@ -253,11 +252,7 @@ class Newsletters extends APIEndpoint {
$this->newslettersRepository->bulkRestore([$newsletter->getId()]);
$this->newslettersRepository->refresh($newsletter);
return $this->successResponse(
$this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
]),
$this->newslettersResponseBuilder->build($newsletter),
['count' => 1]
);
} else {
@ -273,11 +268,7 @@ class Newsletters extends APIEndpoint {
$this->newslettersRepository->bulkTrash([$newsletter->getId()]);
$this->newslettersRepository->refresh($newsletter);
return $this->successResponse(
$this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
]),
$this->newslettersResponseBuilder->build($newsletter),
['count' => 1]
);
} else {
@ -291,7 +282,7 @@ class Newsletters extends APIEndpoint {
$newsletter = $this->getNewsletter($data);
if ($newsletter instanceof NewsletterEntity) {
$this->wp->doAction('mailpoet_api_newsletters_delete_before', [$newsletter->getId()]);
$this->newslettersRepository->bulkDelete([$newsletter->getId()]);
$this->newsletterDeleteController->bulkDelete([(int)$newsletter->getId()]);
$this->wp->doAction('mailpoet_api_newsletters_delete_after', [$newsletter->getId()]);
return $this->successResponse(null, ['count' => 1]);
} else {
@ -308,11 +299,7 @@ class Newsletters extends APIEndpoint {
$duplicate = $this->newsletterSaveController->duplicate($newsletter);
$this->wp->doAction('mailpoet_api_newsletters_duplicate_after', $newsletter, $duplicate);
return $this->successResponse(
$this->newslettersResponseBuilder->build($duplicate, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
]),
$this->newslettersResponseBuilder->build($duplicate),
['count' => 1]
);
} else {
@ -401,7 +388,7 @@ class Newsletters extends APIEndpoint {
$this->newslettersRepository->bulkRestore($ids);
} elseif ($data['action'] === 'delete') {
$this->wp->doAction('mailpoet_api_newsletters_delete_before', $ids);
$this->newslettersRepository->bulkDelete($ids);
$this->newsletterDeleteController->bulkDelete($ids);
$this->wp->doAction('mailpoet_api_newsletters_delete_after', $ids);
} else {
throw UnexpectedValueException::create()

View File

@ -10,19 +10,17 @@ use MailPoet\Config\AccessControl;
use MailPoet\Cron\ActionScheduler\Actions\DaemonTrigger;
use MailPoet\Cron\CronTrigger;
use MailPoet\Cron\Triggers\WordPress;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Models\SendingQueue as SendingQueueModel;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\NewsletterValidator;
use MailPoet\Newsletter\Scheduler\Scheduler;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Segments\SubscribersFinder;
use MailPoet\Settings\SettingsController;
use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoetVendor\Carbon\Carbon;
@ -52,9 +50,6 @@ class SendingQueue extends APIEndpoint {
/** @var NewsletterValidator */
private $newsletterValidator;
/** @var Scheduler */
private $scheduler;
/** @var SettingsController */
private $settings;
@ -71,7 +66,6 @@ class SendingQueue extends APIEndpoint {
SubscribersFinder $subscribersFinder,
ScheduledTasksRepository $scheduledTasksRepository,
MailerFactory $mailerFactory,
Scheduler $scheduler,
SettingsController $settings,
DaemonTrigger $actionSchedulerDaemonTriggerAction,
NewsletterValidator $newsletterValidator,
@ -83,7 +77,6 @@ class SendingQueue extends APIEndpoint {
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->mailerFactory = $mailerFactory;
$this->scheduler = $scheduler;
$this->settings = $settings;
$this->actionSchedulerDaemonTriggerAction = $actionSchedulerDaemonTriggerAction;
$this->newsletterValidator = $newsletterValidator;
@ -118,98 +111,81 @@ class SendingQueue extends APIEndpoint {
]);
}
// check that the sending method has been configured properly by verifying that default mailer can be build
try {
// check that the sending method has been configured properly by verifying that default mailer can be build
$this->mailerFactory->getDefaultMailer();
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
// add newsletter to the sending queue
$queue = SendingQueueModel::joinWithTasks()
->where('queues.newsletter_id', $newsletter->getId())
->whereNull('tasks.status')
->findOne();
$sendingQueue = $this->sendingQueuesRepository->findOneByNewsletterAndTaskStatus($newsletter, null);
if (!empty($queue)) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This newsletter is already being sent.', 'mailpoet'),
]);
}
$scheduledQueue = SendingQueueModel::joinWithTasks()
->where('queues.newsletter_id', $newsletter->getId())
->where('tasks.status', SendingQueueModel::STATUS_SCHEDULED)
->findOne();
if ($scheduledQueue instanceof SendingQueueModel) {
$queue = SendingTask::createFromQueue($scheduledQueue);
} else {
$queue = SendingTask::create();
$queue->newsletterId = $newsletter->getId();
}
$taskModel = $queue->task();
$taskEntity = $this->scheduledTasksRepository->findOneById($taskModel->id);
if (!$taskEntity instanceof ScheduledTaskEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Unable to find scheduled task associated with this newsletter.', 'mailpoet'),
]);
}
WordPress::resetRunInterval();
if ((bool)$newsletter->getOptionValue('isScheduled')) {
// set newsletter status
$newsletter->setStatus(NewsletterEntity::STATUS_SCHEDULED);
// set queue status
$scheduledAt = $this->scheduler->formatDatetimeString($newsletter->getOptionValue('scheduledAt'));
$queue->status = SendingQueueModel::STATUS_SCHEDULED;
$queue->scheduledAt = $scheduledAt;
// we need to refresh the entity here for now while this method still uses Paris
$taskEntity->setStatus(SendingQueueModel::STATUS_SCHEDULED);
$taskEntity->setScheduledAt(new Carbon($scheduledAt));
} else {
$segments = $newsletter->getSegmentIds();
$subscribersCount = $this->subscribersFinder->addSubscribersToTaskFromSegments($taskEntity, $segments, $newsletter->getFilterSegmentId());
if (!$subscribersCount) {
if ($sendingQueue instanceof SendingQueueEntity) {
return $this->errorResponse([
APIError::UNKNOWN => __('There are no subscribers in that list!', 'mailpoet'),
APIError::NOT_FOUND => __('This newsletter is already being sent.', 'mailpoet'),
]);
}
$queue->updateCount();
$queue->status = null;
$queue->scheduledAt = null;
// we need to refresh the entity here for now while this method still uses Paris
$taskEntity->setStatus(null);
$taskEntity->setScheduledAt(null);
$sendingQueue = $this->sendingQueuesRepository->findOneByNewsletterAndTaskStatus($newsletter, ScheduledTaskEntity::STATUS_SCHEDULED);
// set newsletter status
$newsletter->setStatus(NewsletterEntity::STATUS_SENDING);
}
$queue->save();
$this->newsletterRepository->flush();
// refreshing is needed while this method still uses Paris
$this->newsletterRepository->refresh($newsletter);
$latestQueue = $newsletter->getLatestQueue();
if ($latestQueue instanceof SendingQueueEntity) {
$this->sendingQueuesRepository->refresh($latestQueue);
}
if (is_null($sendingQueue)) {
$scheduledTask = new ScheduledTaskEntity();
$scheduledTask->setType(SendingQueueWorker::TASK_TYPE);
$sendingQueue = new SendingQueueEntity();
$sendingQueue->setNewsletter($newsletter);
$sendingQueue->setTask($scheduledTask);
$this->sendingQueuesRepository->persist($sendingQueue);
$this->newsletterRepository->refresh($newsletter);
} else {
$scheduledTask = $sendingQueue->getTask();
}
if (!$scheduledTask instanceof ScheduledTaskEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Unable to find scheduled task associated with this newsletter.', 'mailpoet'),
]);
}
$scheduledTask->setPriority(ScheduledTaskEntity::PRIORITY_MEDIUM);
$this->scheduledTasksRepository->persist($scheduledTask);
$this->scheduledTasksRepository->flush();
WordPress::resetRunInterval();
if ((bool)$newsletter->getOptionValue('isScheduled')) {
// set newsletter status
$newsletter->setStatus(NewsletterEntity::STATUS_SCHEDULED);
// set scheduled task status
$scheduledTask->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
$scheduledTask->setScheduledAt(new Carbon($newsletter->getOptionValue('scheduledAt')));
} else {
$segments = $newsletter->getSegmentIds();
$this->scheduledTasksRepository->refresh($scheduledTask);
$subscribersCount = $this->subscribersFinder->addSubscribersToTaskFromSegments($scheduledTask, $segments, $newsletter->getFilterSegmentId());
if (!$subscribersCount) {
return $this->errorResponse([
APIError::UNKNOWN => __('There are no subscribers in that list!', 'mailpoet'),
]);
}
$this->sendingQueuesRepository->updateCounts($sendingQueue);
$scheduledTask->setStatus(null);
$scheduledTask->setScheduledAt(null);
// set newsletter status
$newsletter->setStatus(NewsletterEntity::STATUS_SENDING);
}
$this->scheduledTasksRepository->persist($scheduledTask);
$this->newsletterRepository->flush();
$errors = $queue->getErrors();
if (!empty($errors)) {
return $this->errorResponse($errors);
} else {
$this->triggerSending($newsletter);
return $this->successResponse(
($newsletter->getLatestQueue() instanceof SendingQueueEntity) ? $this->sendingQueuesResponseBuilder->build($newsletter->getLatestQueue()) : null
);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
}

View File

@ -152,9 +152,6 @@ class Newsletters {
$data['all_sender_domains'] = $this->senderDomainController->getAllSenderDomains();
$data['sender_restrictions'] = [
'lowerLimit' => AuthorizedSenderDomainController::LOWER_LIMIT,
'upperLimit' => AuthorizedSenderDomainController::UPPER_LIMIT,
'isNewUser' => $this->senderDomainController->isNewUser(),
'isEnforcementOfNewRestrictionsInEffect' => $this->senderDomainController->isEnforcementOfNewRestrictionsInEffect(),
'isAuthorizedDomainRequiredForNewCampaigns' => $this->senderDomainController->isAuthorizedDomainRequiredForNewCampaigns(),
'campaignTypes' => NewsletterEntity::CAMPAIGN_TYPES,
];

View File

@ -106,9 +106,6 @@ class Settings {
$data['all_sender_domains'] = $this->senderDomainController->getAllSenderDomains();
$data['sender_restrictions'] = [
'lowerLimit' => AuthorizedSenderDomainController::LOWER_LIMIT,
'upperLimit' => AuthorizedSenderDomainController::UPPER_LIMIT,
'isNewUser' => $this->senderDomainController->isNewUser(),
'isEnforcementOfNewRestrictionsInEffect' => $this->senderDomainController->isEnforcementOfNewRestrictionsInEffect(),
];
}

View File

@ -7,6 +7,7 @@ use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Newsletter\NewsletterDeleteController;
use MailPoet\Newsletter\NewslettersRepository;
class AutomationEditorLoadingHooks {
@ -20,14 +21,18 @@ class AutomationEditorLoadingHooks {
/** @var NewslettersRepository */
private $newslettersRepository;
private NewsletterDeleteController $newsletterDeleteController;
public function __construct(
WordPress $wp,
AutomationStorage $automationStorage,
NewslettersRepository $newslettersRepository
NewslettersRepository $newslettersRepository,
NewsletterDeleteController $newsletterDeleteController
) {
$this->wp = $wp;
$this->automationStorage = $automationStorage;
$this->newslettersRepository = $newslettersRepository;
$this->newsletterDeleteController = $newsletterDeleteController;
}
public function init(): void {
@ -59,7 +64,7 @@ class AutomationEditorLoadingHooks {
continue;
}
$this->newslettersRepository->bulkDelete([$emailId]);
$this->newsletterDeleteController->bulkDelete([$emailId]);
$args = $step->getArgs();
unset($args['email_id']);
$updatedStep = new Step(

View File

@ -4,6 +4,7 @@ namespace MailPoet\Cron;
use MailPoet\Cron\Workers\WorkersFactory;
use MailPoet\Logging\LoggerFactory;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class Daemon {
public $timer;
@ -14,6 +15,9 @@ class Daemon {
/** @var CronWorkerRunner */
private $cronWorkerRunner;
/** @var EntityManager */
private $entityManager;
/** @var WorkersFactory */
private $workersFactory;
@ -23,12 +27,14 @@ class Daemon {
public function __construct(
CronHelper $cronHelper,
CronWorkerRunner $cronWorkerRunner,
EntityManager $entityManager,
WorkersFactory $workersFactory,
LoggerFactory $loggerFactory
) {
$this->timer = microtime(true);
$this->workersFactory = $workersFactory;
$this->cronWorkerRunner = $cronWorkerRunner;
$this->entityManager = $entityManager;
$this->cronHelper = $cronHelper;
$this->loggerFactory = $loggerFactory;
}
@ -40,6 +46,10 @@ class Daemon {
$errors = [];
foreach ($this->getWorkers() as $worker) {
try {
// Clear the entity manager memory for every cron run.
// This avoids using stale data and prevents memory leaks.
$this->entityManager->clear();
if ($worker instanceof CronWorkerInterface) {
$this->cronWorkerRunner->run($worker);
} else {

View File

@ -181,8 +181,9 @@ class SendingQueue {
// pre-process newsletter (render, replace shortcodes/links, etc.)
$newsletter = $this->newsletterTask->preProcessNewsletter($newsletter, $task);
// During pre-processing we may find that the newsletter can't be sent and we delete it including all associated entities
// E.g. post notification history newsletter when there are no posts to send
if (!$newsletter) {
$this->deleteTask($task);
return;
}
@ -575,6 +576,11 @@ class SendingQueue {
}
private function stopProgress(ScheduledTaskEntity $task): void {
// if task is not managed by entity manager, it's already deleted and detached
// it can be deleted in self::processSending method
if (!$this->entityManager->contains($task)) {
return;
}
$task->setInProgress(false);
$this->scheduledTasksRepository->flush();
}

View File

@ -8,7 +8,6 @@ use MailPoet\Mailer\Mailer as MailerInstance;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Mailer\MailerLog;
use MailPoet\Mailer\Methods\MailPoet;
use MailPoet\Models\Subscriber;
class Mailer {
/** @var MailerFactory */
@ -62,9 +61,7 @@ class Mailer {
}
public function prepareSubscriberForSending(SubscriberEntity $subscriber) {
$subscriberModel = Subscriber::findOne($subscriber->getId());
return $this->mailer->formatSubscriberNameAndEmailAddress($subscriberModel);
return $this->mailer->formatSubscriberNameAndEmailAddress($subscriber);
}
public function sendBulk($preparedNewsletters, $preparedSubscribers, $extraParams = []) {

View File

@ -14,11 +14,13 @@ use MailPoet\Entities\SubscriberEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Mailer\MailerLog;
use MailPoet\Newsletter\Links\Links as NewsletterLinks;
use MailPoet\Newsletter\NewsletterDeleteController;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Renderer\PostProcess\OpenTracking;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\RuntimeException;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Statistics\GATracking;
@ -51,6 +53,9 @@ class Newsletter {
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var NewsletterDeleteController */
private $newsletterDeleteController;
/** @var Emoji */
private $emoji;
@ -96,6 +101,7 @@ class Newsletter {
$this->emoji = $emoji;
$this->renderer = ContainerWrapper::getInstance()->get(Renderer::class);
$this->newslettersRepository = ContainerWrapper::getInstance()->get(NewslettersRepository::class);
$this->newsletterDeleteController = ContainerWrapper::getInstance()->get(NewsletterDeleteController::class);
$this->linksTask = ContainerWrapper::getInstance()->get(LinksTask::class);
$this->newsletterLinks = ContainerWrapper::getInstance()->get(NewsletterLinks::class);
$this->sendingQueuesRepository = ContainerWrapper::getInstance()->get(SendingQueuesRepository::class);
@ -134,11 +140,22 @@ class Newsletter {
return $newsletter;
}
/**
* Pre-processes the newsletter before sending.
* - Renders the newsletter
* - Adds tracking
* - Extracts links
* - Checks if the newsletter is a post notification and if it contains at least 1 ALC post.
* If not it deletes the notification history record and all associate entities.
*
* @return NewsletterEntity|false - Returns false only if the newsletter is a post notification history and was deleted.
*
*/
public function preProcessNewsletter(NewsletterEntity $newsletter, ScheduledTaskEntity $task) {
// return the newsletter if it was previously rendered
$queue = $task->getSendingQueue();
if (!$queue) {
return false;
throw new RuntimeException('Cant pre-process newsletter without queue.');
}
if ($queue->getNewsletterRenderedBody() !== null) {
return $newsletter;
@ -191,7 +208,7 @@ class Newsletter {
'no posts in post notification, deleting it',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
$this->newslettersRepository->bulkDelete([(int)$newsletter->getId()]);
$this->newsletterDeleteController->bulkDelete([(int)$newsletter->getId()]);
return false;
}
// extract and save newsletter posts

View File

@ -40,4 +40,20 @@ class NewsletterLinkRepository extends Repository {
}
return null;
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(NewsletterLinkEntity::class, 'l')
->where('l.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (NewsletterLinkEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -65,4 +65,20 @@ class StatsNotificationsRepository extends Repository {
WHERE sn.id IS NULL AND st.type = :taskType;
", ['taskType' => Worker::TASK_TYPE]);
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(StatsNotificationEntity::class, 'n')
->where('n.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (StatsNotificationEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -543,6 +543,7 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\Newsletter\ApiDataSanitizer::class)->setPublic(true);
$container->autowire(\MailPoet\Newsletter\AutomatedLatestContent::class)->setPublic(true);
$container->autowire(\MailPoet\Newsletter\NewsletterSaveController::class)->setPublic(true);
$container->autowire(\MailPoet\Newsletter\NewsletterDeleteController::class)->setPublic(true);
$container->autowire(\MailPoet\Newsletter\NewsletterPostsRepository::class)->setPublic(true);
$container->autowire(\MailPoet\Newsletter\NewslettersRepository::class)->setPublic(true);
$container->autowire(\MailPoet\Newsletter\AutomaticEmailsRepository::class)->setPublic(true);

View File

@ -111,6 +111,19 @@ abstract class Repository {
$this->entityManager->refresh($entity);
}
/**
* @param callable(T): bool|null $filter
*/
public function refreshAll(callable $filter = null): void {
$entities = $this->getAllFromIdentityMap();
foreach ($entities as $entity) {
if ($filter && !$filter($entity)) {
continue;
}
$this->entityManager->refresh($entity);
}
}
public function flush() {
$this->entityManager->flush();
}
@ -130,13 +143,8 @@ abstract class Repository {
* @param callable(T): bool|null $filter
*/
public function detachAll(callable $filter = null): void {
$className = $this->getEntityClassName();
$rootClassName = $this->entityManager->getClassMetadata($className)->rootEntityName;
$entities = $this->entityManager->getUnitOfWork()->getIdentityMap()[$rootClassName] ?? [];
$entities = $this->getAllFromIdentityMap();
foreach ($entities as $entity) {
if (!($entity instanceof $className)) {
continue;
}
if ($filter && !$filter($entity)) {
continue;
}
@ -144,22 +152,19 @@ abstract class Repository {
}
}
/**
* @param class-string<object> $className
* @param callable|null $filter
*/
public function detachEntitiesOfType($className, callable $filter = null): void {
/** @return T[] */
public function getAllFromIdentityMap(): array {
$className = $this->getEntityClassName();
$rootClassName = $this->entityManager->getClassMetadata($className)->rootEntityName;
$entities = $this->entityManager->getUnitOfWork()->getIdentityMap()[$rootClassName] ?? [];
$result = [];
foreach ($entities as $entity) {
if (!($entity instanceof $className)) {
continue;
if ($entity instanceof $className) {
$result[] = $entity;
}
if ($filter && !$filter($entity)) {
continue;
}
$this->entityManager->detach($entity);
}
return $result;
}
/**

View File

@ -36,19 +36,21 @@ class StatisticsNewsletterEntity {
private $subscriber;
/**
* @ORM\Column(type="datetimetz", nullable=true)
* @var \DateTimeInterface|null
* @ORM\Column(type="datetimetz", nullable=false)
* @var \DateTimeInterface
*/
private $sentAt;
public function __construct(
NewsletterEntity $newsletter,
SendingQueueEntity $queue,
SubscriberEntity $subscriber
SubscriberEntity $subscriber,
\DateTimeInterface $sentAt = null
) {
$this->newsletter = $newsletter;
$this->queue = $queue;
$this->subscriber = $subscriber;
$this->sentAt = $sentAt ?: new \DateTimeImmutable();
}
/**

View File

@ -20,7 +20,7 @@ class StatisticsWooCommercePurchaseEntity {
/**
* @ORM\ManyToOne(targetEntity="MailPoet\Entities\NewsletterEntity")
* @ORM\JoinColumn(name="newsletter_id", referencedColumnName="id")
* @ORM\JoinColumn(name="newsletter_id", referencedColumnName="id", nullable=true)
* @var NewsletterEntity|null
*/
private $newsletter;

View File

@ -69,12 +69,19 @@ class FormsRepository extends Repository {
return 0;
}
return $this->entityManager->createQueryBuilder()
$result = $this->entityManager->createQueryBuilder()
->update(FormEntity::class, 'f')
->set('f.deletedAt', 'CURRENT_TIMESTAMP()')
->where('f.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()->execute();
// update was done via DQL, make sure the entities are also refreshed in the entity manager
$this->refreshAll(function (FormEntity $entity) use ($ids) {
return in_array($entity->getId(), $ids, true);
});
return $result;
}
public function bulkRestore(array $ids): int {
@ -82,13 +89,20 @@ class FormsRepository extends Repository {
return 0;
}
return $this->entityManager->createQueryBuilder()
$result = $this->entityManager->createQueryBuilder()
->update(FormEntity::class, 'f')
->set('f.deletedAt', ':deletedAt')
->where('f.id IN (:ids)')
->setParameter('deletedAt', null)
->setParameter('ids', $ids)
->getQuery()->execute();
// update was done via DQL, make sure the entities are also refreshed in the entity manager
$this->refreshAll(function (FormEntity $entity) use ($ids) {
return in_array($entity->getId(), $ids, true);
});
return $result;
}
public function bulkDelete(array $ids): int {

View File

@ -4,7 +4,6 @@ namespace MailPoet\Mailer;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Mailer\Methods\MailerMethod;
use MailPoet\Models\Subscriber;
class Mailer {
/** @var MailerMethod */
@ -25,20 +24,23 @@ class Mailer {
}
public function send($newsletter, $subscriber, $extraParams = []) {
// This if adds support for code that calls this method to use SubscriberEntity while the Mailer class is still using the old model.
// Once we add support for SubscriberEntity in the Mailer class, this if can be removed.
if ($subscriber instanceof SubscriberEntity) {
$subscriber = Subscriber::findOne($subscriber->getId());
}
$subscriber = $this->formatSubscriberNameAndEmailAddress($subscriber);
return $this->mailerMethod->send($newsletter, $subscriber, $extraParams);
}
/**
* @param \MailPoet\Models\Subscriber|array|string $subscriber
* @param SubscriberEntity|array|string $subscriber
*/
public function formatSubscriberNameAndEmailAddress($subscriber) {
$subscriber = (is_object($subscriber)) ? $subscriber->asArray() : $subscriber;
if ($subscriber instanceof SubscriberEntity) {
$prepareSubscriber = [];
$prepareSubscriber['email'] = $subscriber->getEmail();
$prepareSubscriber['first_name'] = $subscriber->getFirstName();
$prepareSubscriber['last_name'] = $subscriber->getLastName();
$subscriber = $prepareSubscriber;
}
if (!is_array($subscriber)) return $subscriber;
if (isset($subscriber['address'])) $subscriber['email'] = $subscriber['address'];
$firstName = (isset($subscriber['first_name'])) ? $subscriber['first_name'] : '';

View File

@ -0,0 +1,39 @@
<?php declare(strict_types = 1);
namespace MailPoet\Migrations\App;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Migrator\AppMigration;
/**
* Due to a bug https://mailpoet.atlassian.net/browse/MAILPOET-5886
* The status of newsletters was not updated to sent when the task was completed
* In this migration we find newsletters with status sending and their tasks are completed and update their status to sent
*/
class Migration_20240202_130053_App extends AppMigration {
public function run(): void {
$affectedNewsletterIds = $this->entityManager->createQueryBuilder()
->select('n.id')
->from(NewsletterEntity::class, 'n')
->join('n.queues', 'q')
->join('q.task', 't')
->where('n.status = :status_sending')
->andWhere('t.status = :status_completed')
->setParameter('status_sending', NewsletterEntity::STATUS_SENDING)
->setParameter('status_completed', ScheduledTaskEntity::STATUS_COMPLETED)
->getQuery()
->getArrayResult();
$affectedNewsletterIds = array_column($affectedNewsletterIds, 'id');
$this->entityManager->createQueryBuilder()
->update(NewsletterEntity::class, 'n')
->set('n.status', ':status_sent')
->where('n.id IN (:ids)')
->setParameter('status_sent', NewsletterEntity::STATUS_SENT)
->setParameter('ids', $affectedNewsletterIds)
->getQuery()
->execute();
}
}

View File

@ -0,0 +1,189 @@
<?php declare(strict_types = 1);
namespace MailPoet\Migrations\App;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Migrator\AppMigration;
use MailPoetVendor\Doctrine\DBAL\Connection;
/**
* We've had a set of bugs where campaign type newsletters (see NewsletterEntity::CAMPAIGN_TYPES),
* such as post notifications, were getting stuck in the following state:
* - The newsletter was in the "sending" state.
* - The task failed to complete and ended up in the "invalid" state.
*
* This migration completes tasks that sent out all emails
* and pauses those that have unprocessed subscribers.
*/
class Migration_20240207_105912_App extends AppMigration {
public function run(): void {
$this->pauseInvalidTasksWithUnprocessedSubscribers();
$this->completeInvalidTasksWithAllSubscribersProcessed();
$this->backfillMissingDataForMigratedNewsletters();
}
private function pauseInvalidTasksWithUnprocessedSubscribers(): void {
$ids = $this->entityManager->createQueryBuilder()
->select('DISTINCT t.id')
->from(ScheduledTaskEntity::class, 't')
->join('t.subscribers', 's', 'WITH', 's.processed = :unprocessed')
->join('t.sendingQueue', 'q')
->join('q.newsletter', 'n')
->where('t.deletedAt IS NULL')
->andWhere('t.status = :invalid')
->andWhere('n.deletedAt IS NULL')
->andWhere('n.status = :sending')
->andWhere('n.type IN (:campaignTypes)')
->setParameter('unprocessed', ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED)
->setParameter('invalid', ScheduledTaskEntity::STATUS_INVALID)
->setParameter('sending', NewsletterEntity::STATUS_SENDING)
->setParameter('campaignTypes', NewsletterEntity::CAMPAIGN_TYPES)
->getQuery()
->getSingleColumnResult();
$this->entityManager->createQueryBuilder()
->update(ScheduledTaskEntity::class, 't')
->set('t.status', ':paused')
->where('t.id IN (:ids)')
->setParameter('paused', ScheduledTaskEntity::STATUS_PAUSED)
->setParameter('ids', $ids)
->getQuery()
->execute();
}
private function completeInvalidTasksWithAllSubscribersProcessed(): void {
$ids = $this->entityManager->createQueryBuilder()
->select('DISTINCT t.id, n.id AS nid, t.updatedAt')
->from(ScheduledTaskEntity::class, 't')
->leftJoin('t.subscribers', 's', 'WITH', 's.processed = :unprocessed')
->join('t.sendingQueue', 'q')
->join('q.newsletter', 'n')
->where('t.deletedAt IS NULL')
->andWhere('t.status = :invalid')
->andWhere('s.task IS NULL')
->andWhere('n.deletedAt IS NULL')
->andWhere('n.status = :sending')
->andWhere('n.type IN (:campaignTypes)')
->setParameter('unprocessed', ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED)
->setParameter('invalid', ScheduledTaskEntity::STATUS_INVALID)
->setParameter('sending', NewsletterEntity::STATUS_SENDING)
->setParameter('campaignTypes', NewsletterEntity::CAMPAIGN_TYPES)
->getQuery()
->getSingleColumnResult();
// update sending queue counts
$this->entityManager->createQueryBuilder()
->update(SendingQueueEntity::class, 'q')
->set('q.countProcessed', 'q.countTotal')
->set('q.countToProcess', 0)
->where('q.task IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// complete the invalid tasks
$this->entityManager->createQueryBuilder()
->update(ScheduledTaskEntity::class, 't')
->set('t.status', ':completed')
->where('t.id IN (:ids)')
->setParameter('completed', ScheduledTaskEntity::STATUS_COMPLETED)
->setParameter('ids', $ids)
->getQuery()
->execute();
// mark newsletters as sent, update "sentAt" (DBAL needed to be able to use JOIN)
$newslettersTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
$scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
$scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
$sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
$this->entityManager->getConnection()->executeStatement(
"
UPDATE $newslettersTable n
JOIN $sendingQueuesTable q ON n.id = q.newsletter_id
JOIN $scheduledTasksTable t ON q.task_id = t.id
SET
n.status = :sent,
n.sent_at = COALESCE(
(
-- use 'updated_at' of processed subscriber with the highest ID ('MAX(subscriber_id)' can use index)
SELECT updated_at FROM $scheduledTaskSubscribersTable WHERE task_id = t.id AND subscriber_id = (
SELECT MAX(subscriber_id) FROM $scheduledTaskSubscribersTable WHERE task_id = t.id
)
),
t.updated_at
)
WHERE t.id IN (:ids)
",
['sent' => NewsletterEntity::STATUS_SENT, 'ids' => $ids],
['ids' => Connection::PARAM_INT_ARRAY]
);
}
private function backfillMissingDataForMigratedNewsletters(): void {
// In https://mailpoet.atlassian.net/browse/MAILPOET-5886 we fixed missing "sent" status
// by https://github.com/mailpoet/mailpoet/pull/5416, but didn't backfill missing data.
// get affected newsletter IDs
$ids = $this->entityManager->createQueryBuilder()
->select('n.id')
->from(NewsletterEntity::class, 'n')
->where('n.status = :sent')
->andWhere('n.sentAt IS NULL')
->setParameter('sent', NewsletterEntity::STATUS_SENT)
->getQuery()
->getSingleColumnResult();
// get missing newsletter statistics IDs
$data = $this->entityManager->createQueryBuilder()
->select('IDENTITY(q.newsletter) AS nid, q.id AS qid, IDENTITY(s.subscriber) AS sid, s.updatedAt AS sentAt')
->from(SendingQueueEntity::class, 'q')
->join('q.task', 't')
->join('t.subscribers', 's')
->leftJoin(StatisticsNewsletterEntity::class, 'ns', 'WITH', 'ns.queue = q AND ns.subscriber = s.subscriber')
->where('q.newsletter IN (:ids)')
->andWhere('ns.id IS NULL')
->andWhere('s.processed = :processed')
->setParameter('ids', $ids)
->setParameter('processed', ScheduledTaskSubscriberEntity::STATUS_PROCESSED)
->getQuery()
->getResult();
// insert missing newsletter statistics
$newsletterStatisticsTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
foreach ($data as $row) {
$this->entityManager->getConnection()->executeStatement("
INSERT IGNORE INTO $newsletterStatisticsTable (newsletter_id, queue_id, subscriber_id, sent_at)
VALUES (?, ?, ?, ?)
", [$row['nid'], $row['qid'], $row['sid'], $row['sentAt']->format('Y-m-d H:i:s')]);
}
// add missing "sentAt" (DBAL needed to be able to use JOIN)
$newslettersTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
$scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
$scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
$sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
$this->entityManager->getConnection()->executeStatement(
"
UPDATE $newslettersTable n
JOIN $sendingQueuesTable q ON n.id = q.newsletter_id
JOIN $scheduledTasksTable t ON q.task_id = t.id
SET n.sent_at = COALESCE(
(
-- use 'updated_at' of processed subscriber with the highest ID ('MAX(subscriber_id)' can use index)
SELECT updated_at FROM $scheduledTaskSubscribersTable WHERE task_id = t.id AND subscriber_id = (
SELECT MAX(subscriber_id) FROM $scheduledTaskSubscribersTable WHERE task_id = t.id
)
),
t.updated_at
)
WHERE q.newsletter_id IN (:ids)
",
['ids' => $ids],
['ids' => Connection::PARAM_INT_ARRAY]
);
}
}

View File

@ -0,0 +1,22 @@
<?php declare(strict_types = 1);
namespace MailPoet\Migrations\Db;
use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\Migrator\DbMigration;
class Migration_20240119_113943_Db extends DbMigration {
public function run(): void {
$table = $this->getTableName(StatisticsWooCommercePurchaseEntity::class);
if (!$this->tableExists($table)) {
return;
}
// make "newsletter_id" nullable
$this->connection->executeStatement("ALTER TABLE $table CHANGE newsletter_id newsletter_id int(11) unsigned NULL");
// update data
$this->connection->executeStatement("UPDATE $table SET newsletter_id = NULL WHERE newsletter_id = 0");
}
}

View File

@ -4,10 +4,10 @@ namespace MailPoet\Models;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\NewsletterDeleteController;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Options\NewsletterOptionFieldsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security;
@ -142,7 +142,7 @@ class Newsletter extends Model {
public function delete() {
trigger_error('Calling Newsletter::delete() is deprecated and will be removed. Use \MailPoet\Newsletter\NewslettersRepository instead.', E_USER_DEPRECATED);
ContainerWrapper::getInstance()->get(NewslettersRepository::class)->bulkDelete([$this->id]);
ContainerWrapper::getInstance()->get(NewsletterDeleteController::class)->bulkDelete([$this->id]);
return null;
}
@ -218,8 +218,11 @@ class Newsletter extends Model {
return $this;
}
/**
* @deprecated This method is deprecated. \MailPoet\Entities\NewsletterEntity::getLatestQueue() instead. This method can be removed after 2024-05-30.
*/
public function getQueue($columns = '*') {
return SendingTask::getByNewsletterId($this->id);
self::deprecationError(__METHOD__);
}
public function getBodyString(): string {
@ -232,7 +235,11 @@ class Newsletter extends Model {
return $this->body;
}
/**
* @deprecated This method is deprecated. It method can be removed after 2024-05-30.
*/
public function withSendingQueue() {
self::deprecationError(__METHOD__);
$queue = $this->getQueue();
if ($queue === false) {
$this->queue = false;

View File

@ -0,0 +1,156 @@
<?php declare(strict_types = 1);
namespace MailPoet\Newsletter;
use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
use MailPoet\Cron\Workers\StatsNotifications\StatsNotificationsRepository;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatsNotificationEntity;
use MailPoet\Newsletter\Options\NewsletterOptionsRepository;
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Statistics\StatisticsClicksRepository;
use MailPoet\Statistics\StatisticsNewslettersRepository;
use MailPoet\Statistics\StatisticsOpensRepository;
use MailPoet\Statistics\StatisticsWooCommercePurchasesRepository;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Doctrine\ORM\EntityManager;
use Throwable;
class NewsletterDeleteController {
private EntityManager $entityManager;
private NewslettersRepository $newslettersRepository;
private NewsletterLinkRepository $newsletterLinkRepository;
private NewsletterOptionsRepository $newsletterOptionsRepository;
private NewsletterPostsRepository $newsletterPostsRepository;
private NewsletterSegmentRepository $newsletterSegmentRepository;
private ScheduledTasksRepository $scheduledTasksRepository;
private ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository;
private SendingQueuesRepository $sendingQueuesRepository;
private StatisticsClicksRepository $statisticsClicksRepository;
private StatisticsNewslettersRepository $statisticsNewslettersRepository;
private StatisticsOpensRepository $statisticsOpensRepository;
private StatisticsWooCommercePurchasesRepository $statisticsWooCommercePurchasesRepository;
private StatsNotificationsRepository $statsNotificationsRepository;
private WPFunctions $wp;
public function __construct(
EntityManager $entityManager,
NewslettersRepository $newslettersRepository,
NewsletterLinkRepository $newsletterLinkRepository,
NewsletterOptionsRepository $newsletterOptionsRepository,
NewsletterPostsRepository $newsletterPostsRepository,
NewsletterSegmentRepository $newsletterSegmentRepository,
ScheduledTasksRepository $scheduledTasksRepository,
ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository,
SendingQueuesRepository $sendingQueuesRepository,
StatisticsClicksRepository $statisticsClicksRepository,
StatisticsNewslettersRepository $statisticsNewslettersRepository,
StatisticsOpensRepository $statisticsOpensRepository,
StatisticsWooCommercePurchasesRepository $statisticsWooCommercePurchasesRepository,
StatsNotificationsRepository $statsNotificationsRepository,
WPFunctions $wp
) {
$this->entityManager = $entityManager;
$this->newslettersRepository = $newslettersRepository;
$this->newsletterLinkRepository = $newsletterLinkRepository;
$this->newsletterOptionsRepository = $newsletterOptionsRepository;
$this->newsletterPostsRepository = $newsletterPostsRepository;
$this->newsletterSegmentRepository = $newsletterSegmentRepository;
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->scheduledTaskSubscribersRepository = $scheduledTaskSubscribersRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->statisticsClicksRepository = $statisticsClicksRepository;
$this->statisticsNewslettersRepository = $statisticsNewslettersRepository;
$this->statisticsOpensRepository = $statisticsOpensRepository;
$this->statisticsWooCommercePurchasesRepository = $statisticsWooCommercePurchasesRepository;
$this->statsNotificationsRepository = $statsNotificationsRepository;
$this->wp = $wp;
}
/** @param int[] $ids */
public function bulkDelete(array $ids): int {
if (!$ids) {
return 0;
}
// Fetch children ids for deleting
$childrenIds = $this->newslettersRepository->fetchChildrenIds($ids);
$ids = array_merge($ids, $childrenIds);
$this->entityManager->beginTransaction();
try {
// Delete statistics data
$this->statisticsNewslettersRepository->deleteByNewsletterIds($ids);
$this->statisticsOpensRepository->deleteByNewsletterIds($ids);
$this->statisticsClicksRepository->deleteByNewsletterIds($ids);
// Update WooCommerce statistics and remove newsletter and click id
$this->statisticsWooCommercePurchasesRepository->removeNewsletterDataByNewsletterIds($ids);
// Delete newsletter posts, options, links, and segments
$this->newsletterPostsRepository->deleteByNewsletterIds($ids);
$this->newsletterOptionsRepository->deleteByNewsletterIds($ids);
$this->newsletterLinkRepository->deleteByNewsletterIds($ids);
$this->newsletterSegmentRepository->deleteByNewsletterIds($ids);
// Delete stats notifications and related tasks
/** @var string[] $taskIds */
$taskIds = $this->entityManager->createQueryBuilder()
->select('IDENTITY(sn.task)')
->from(StatsNotificationEntity::class, 'sn')
->where('sn.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->getSingleColumnResult();
$taskIds = array_map('intval', $taskIds);
$this->scheduledTasksRepository->deleteByIds($taskIds);
$this->statsNotificationsRepository->deleteByNewsletterIds($ids);
// Delete scheduled task subscribers, scheduled tasks, and sending queues
/** @var string[] $taskIds */
$taskIds = $this->entityManager->createQueryBuilder()
->select('IDENTITY(q.task)')
->from(SendingQueueEntity::class, 'q')
->where('q.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->getSingleColumnResult();
$taskIds = array_map('intval', $taskIds);
$this->scheduledTaskSubscribersRepository->deleteByTaskIds($taskIds);
$this->scheduledTasksRepository->deleteByIds($taskIds);
$this->sendingQueuesRepository->deleteByNewsletterIds($ids);
// Fetch WP Posts IDs and delete them
/** @var string[] $wpPostIds */
$wpPostIds = $this->entityManager->createQueryBuilder()
->select('IDENTITY(n.wpPost) AS id')
->from(NewsletterEntity::class, 'n')
->where('n.id IN (:ids)')
->andWhere('n.wpPost IS NOT NULL')
->setParameter('ids', $ids)
->getQuery()
->getSingleColumnResult();
$wpPostIds = array_map('intval', $wpPostIds);
foreach ($wpPostIds as $wpPostId) {
$this->wp->wpDeletePost($wpPostId, true);
}
// Delete newsletter entities
$this->newslettersRepository->deleteByIds($ids);
$this->entityManager->commit();
} catch (Throwable $e) {
$this->entityManager->rollback();
throw $e;
}
return count($ids);
}
}

View File

@ -12,4 +12,20 @@ class NewsletterPostsRepository extends Repository {
protected function getEntityClassName() {
return NewsletterPostEntity::class;
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(NewsletterPostEntity::class, 'p')
->where('p.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (NewsletterPostEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -9,22 +9,12 @@ use MailPoet\AutomaticEmails\WooCommerce\Events\PurchasedInCategory;
use MailPoet\AutomaticEmails\WooCommerce\Events\PurchasedProduct;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterLinkEntity;
use MailPoet\Entities\NewsletterOptionEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\NewsletterPostEntity;
use MailPoet\Entities\NewsletterSegmentEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\Entities\StatsNotificationEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\DBAL\Connection;
use MailPoetVendor\Doctrine\ORM\EntityManager;
@ -34,19 +24,13 @@ use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
* @extends Repository<NewsletterEntity>
*/
class NewslettersRepository extends Repository {
/** @var LoggerFactory */
private $loggerFactory;
/** @var WPFunctions */
private $wp;
private LoggerFactory $loggerFactory;
public function __construct(
EntityManager $entityManager,
WPFunctions $wp
EntityManager $entityManager
) {
parent::__construct($entityManager);
$this->loggerFactory = LoggerFactory::getInstance();
$this->wp = $wp;
}
protected function getEntityClassName() {
@ -360,161 +344,19 @@ class NewslettersRepository extends Repository {
return count($ids);
}
public function bulkDelete(array $ids) {
if (empty($ids)) {
return 0;
}
// Fetch children ids for deleting
$childrenIds = $this->fetchChildrenIds($ids);
$ids = array_merge($ids, $childrenIds);
/** @param int[] $ids */
public function deleteByIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(NewsletterEntity::class, 'n')
->where('n.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
$isRelatedNewsletterToBeDeleted = function($entity) use ($ids): bool {
if (is_string($entity) || !method_exists($entity, 'getNewsletter')) {
return false;
}
$newsletter = $entity->getNewsletter();
$newsletterId = $newsletter ? $newsletter->getId() : null;
return in_array($newsletterId, $ids, true);
};
$this->entityManager->transactional(function (EntityManager $entityManager) use ($ids, $isRelatedNewsletterToBeDeleted) {
// Delete statistics data
$newsletterStatisticsTable = $entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
$entityManager->getConnection()->executeStatement("
DELETE s FROM $newsletterStatisticsTable s
WHERE s.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
$statisticsOpensTable = $entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
$entityManager->getConnection()->executeStatement("
DELETE s FROM $statisticsOpensTable s
WHERE s.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
$statisticsClicksTable = $entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
$entityManager->getConnection()->executeStatement("
DELETE s FROM $statisticsClicksTable s
WHERE s.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Update WooCommerce statistics and remove newsletter and click id
$statisticsPurchasesTable = $entityManager->getClassMetadata(StatisticsWooCommercePurchaseEntity::class)->getTableName();
$entityManager->getConnection()->executeStatement("
UPDATE $statisticsPurchasesTable s
SET s.`newsletter_id` = 0 WHERE s.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Delete newsletter posts
$postsTable = $entityManager->getClassMetadata(NewsletterPostEntity::class)->getTableName();
$entityManager->getConnection()->executeStatement("
DELETE np FROM $postsTable np
WHERE np.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Delete newsletter options
$optionsTable = $entityManager->getClassMetadata(NewsletterOptionEntity::class)->getTableName();
$entityManager->getConnection()->executeStatement("
DELETE no FROM $optionsTable no
WHERE no.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Delete newsletter links
$linksTable = $entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
$entityManager->getConnection()->executeStatement("
DELETE nl FROM $linksTable nl
WHERE nl.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Delete stats notifications tasks
$scheduledTasksTable = $entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
$statsNotificationsTable = $entityManager->getClassMetadata(StatsNotificationEntity::class)->getTableName();
$taskIds = $entityManager->getConnection()->executeQuery("
SELECT task_id FROM $statsNotificationsTable sn
WHERE sn.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY])->fetchAllAssociative();
$taskIds = array_column($taskIds, 'task_id');
$entityManager->getConnection()->executeStatement("
DELETE st FROM $scheduledTasksTable st
WHERE st.`id` IN (:ids)
", ['ids' => $taskIds], ['ids' => Connection::PARAM_INT_ARRAY]);
// Delete stats notifications
$entityManager->getConnection()->executeStatement("
DELETE sn FROM $statsNotificationsTable sn
WHERE sn.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Delete scheduled tasks and scheduled task subscribers
$sendingQueueTable = $entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
$scheduledTaskSubscribersTable = $entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
// Delete scheduled tasks subscribers
$entityManager->getConnection()->executeStatement("
DELETE ts FROM $scheduledTaskSubscribersTable ts
JOIN $scheduledTasksTable t ON t.`id` = ts.`task_id`
JOIN $sendingQueueTable q ON q.`task_id` = t.`id`
WHERE q.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
$entityManager->getConnection()->executeStatement("
DELETE t FROM $scheduledTasksTable t
JOIN $sendingQueueTable q ON t.`id` = q.`task_id`
WHERE q.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Delete sending queues
$entityManager->getConnection()->executeStatement("
DELETE q FROM $sendingQueueTable q
WHERE q.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Delete newsletter segments
$newsletterSegmentsTable = $entityManager->getClassMetadata(NewsletterSegmentEntity::class)->getTableName();
$entityManager->getConnection()->executeStatement("
DELETE ns FROM $newsletterSegmentsTable ns
WHERE ns.`newsletter_id` IN (:ids)
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
// Fetch WP Posts IDs and delete them
/** @var int[] $wpPostsIds */
$wpPostsIds = $entityManager->createQueryBuilder()->select('wpp.id')
->from(NewsletterEntity::class, 'n')
->join('n.wpPost', 'wpp')
->where('n.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()->getSingleColumnResult();
foreach ($wpPostsIds as $wpPostId) {
$this->wp->wpDeletePost(intval($wpPostId), true);
}
// Delete newsletter entities
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->delete(NewsletterEntity::class, 'n')
->where('n.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()->execute();
$entityTypesToBeDetached = [
StatisticsNewsletterEntity::class,
StatisticsOpenEntity::class,
StatisticsClickEntity::class,
NewsletterPostEntity::class,
NewsletterOptionEntity::class,
NewsletterLinkEntity::class,
StatsNotificationEntity::class,
SendingQueueEntity::class,
ScheduledTaskSubscriberEntity::class,
NewsletterSegmentEntity::class,
];
foreach ($entityTypesToBeDetached as $entityType) {
$this->detachEntitiesOfType($entityType, $isRelatedNewsletterToBeDeleted);
}
$this->detachEntitiesOfType(ScheduledTaskEntity::class, function($entity) use ($taskIds): bool {
return !is_string($entity) && method_exists($entity, 'getId') && in_array($entity->getId(), $taskIds, true);
});
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (NewsletterEntity $entity) use ($ids) {
return in_array($entity->getId(), $ids, true);
});
return count($ids);
}
/**
@ -688,12 +530,19 @@ class NewslettersRepository extends Repository {
$this->flush();
}
private function fetchChildrenIds(array $parentIds) {
$ids = $this->entityManager->createQueryBuilder()->select('n.id')
/**
* @param int[] $parentIds
* @return int[]
*/
public function fetchChildrenIds(array $parentIds): array {
/** @var string[] $ids */
$ids = $this->entityManager->createQueryBuilder()
->select('n.id')
->from(NewsletterEntity::class, 'n')
->where('n.parent IN (:ids)')
->setParameter('ids', $parentIds)
->getQuery()->getScalarResult();
return array_column($ids, 'id');
->getQuery()
->getSingleColumnResult();
return array_map('intval', $ids);
}
}

View File

@ -52,4 +52,20 @@ class NewsletterOptionsRepository extends Repository {
->setParameter('segmentIds', $segmentIds)
->getQuery()->getResult();
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(NewsletterOptionEntity::class, 'o')
->where('o.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (NewsletterOptionEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -65,4 +65,20 @@ class NewsletterSegmentRepository extends Repository {
}
return $nameMap;
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(NewsletterSegmentEntity::class, 's')
->where('s.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (NewsletterSegmentEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -94,6 +94,11 @@ class ScheduledTaskSubscribersRepository extends Repository {
->setParameter('task', $task)
->getQuery()
->execute();
// update was done via DQL, make sure the entities are also refreshed in the entity manager
$this->refreshAll(function (ScheduledTaskSubscriberEntity $entity) use ($task, $subscriberIds) {
return $entity->getTask() === $task && in_array($entity->getSubscriberId(), $subscriberIds, true);
});
}
$this->checkCompleted($task);
@ -118,6 +123,22 @@ class ScheduledTaskSubscribersRepository extends Repository {
$stmt->executeQuery();
}
/** @param int[] $ids */
public function deleteByTaskIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(ScheduledTaskSubscriberEntity::class, 'sts')
->where('sts.task IN (:taskIds)')
->setParameter('taskIds', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (ScheduledTaskSubscriberEntity $entity) use ($ids) {
$task = $entity->getTask();
return $task && in_array($task->getId(), $ids, true);
});
}
public function deleteByScheduledTask(ScheduledTaskEntity $scheduledTask): void {
$this->entityManager->createQueryBuilder()
->delete(ScheduledTaskSubscriberEntity::class, 'sts')

View File

@ -308,6 +308,11 @@ class ScheduledTasksRepository extends Repository {
->setParameter('ids', $ids, Connection::PARAM_INT_ARRAY)
->getQuery()
->execute();
// update was done via DQL, make sure the entities are also refreshed in the entity manager
$this->refreshAll(function (ScheduledTaskEntity $entity) use ($ids) {
return in_array($entity->getId(), $ids, true);
});
}
/**
@ -337,6 +342,21 @@ class ScheduledTasksRepository extends Repository {
$this->flush();
}
/** @param int[] $ids */
public function deleteByIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(ScheduledTaskEntity::class, 't')
->where('t.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (ScheduledTaskEntity $entity) use ($ids) {
return in_array($entity->getId(), $ids, true);
});
}
protected function findByTypeAndStatus($type, $status, $limit = null, $future = false) {
$queryBuilder = $this->doctrineRepository->createQueryBuilder('st')
->select('st')

View File

@ -49,17 +49,28 @@ class SendingQueuesRepository extends Repository {
return SendingQueueEntity::class;
}
public function findOneByNewsletterAndTaskStatus(NewsletterEntity $newsletter, string $status): ?SendingQueueEntity {
return $this->entityManager->createQueryBuilder()
/**
* @param NewsletterEntity $newsletter
* @param string|null $status
* @return SendingQueueEntity|null
* @throws \MailPoetVendor\Doctrine\ORM\NonUniqueResultException
*/
public function findOneByNewsletterAndTaskStatus(NewsletterEntity $newsletter, $status): ?SendingQueueEntity {
$queryBuilder = $this->entityManager->createQueryBuilder()
->select('s')
->from(SendingQueueEntity::class, 's')
->join('s.task', 't')
->where('t.status = :status')
->andWhere('s.newsletter = :newsletter')
->setParameter('status', $status)
->setParameter('newsletter', $newsletter)
->getQuery()
->getOneOrNullResult();
->setParameter('newsletter', $newsletter);
if (is_null($status)) {
$queryBuilder->andWhere('t.status IS NULL');
} else {
$queryBuilder->andWhere('t.status = :status')
->setParameter('status', $status);
}
return $queryBuilder->getQuery()->getOneOrNullResult();
}
public function countAllByNewsletterAndTaskStatus(NewsletterEntity $newsletter, string $status): int {
@ -227,4 +238,20 @@ class SendingQueuesRepository extends Repository {
}
$this->entityManager->flush();
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(SendingQueueEntity::class, 'q')
->where('q.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (SendingQueueEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -93,6 +93,11 @@ class NewsletterTemplatesRepository extends Repository {
->setParameter('recentIds', array_column($recentIds, 'id'))
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (NewsletterTemplateEntity $entity) use ($recentIds) {
return $entity->getCategories() === self::RECENTLY_SENT_CATEGORIES && !in_array($entity->getId(), $recentIds, true);
});
}
public function getRecentlySentCount(): int {

View File

@ -69,7 +69,7 @@ class SegmentsRepository extends Repository {
->getResult();
}
public function getWPUsersSegment(): ?SegmentEntity {
public function getWPUsersSegment(): SegmentEntity {
$segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WP_USERS]);
if (!$segment) {

View File

@ -6,9 +6,7 @@ use MailPoet\Config\SubscriberChangesNotifier;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Models\Segment;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberSegment;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Newsletter\Scheduler\WelcomeScheduler;
use MailPoet\Services\Validator;
use MailPoet\Settings\SettingsController;
@ -19,7 +17,7 @@ use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Idiorm\ORM;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class WP {
@ -43,6 +41,18 @@ class WP {
/** @var Validator */
private $validator;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var EntityManager */
private $entityManager;
/** @var string */
private $subscribersTable;
/** @var \MailPoetVendor\Doctrine\DBAL\Connection */
private $databaseConnection;
public function __construct(
WPFunctions $wp,
WelcomeScheduler $welcomeScheduler,
@ -50,7 +60,9 @@ class WP {
SubscribersRepository $subscribersRepository,
SubscriberSegmentRepository $subscriberSegmentRepository,
SubscriberChangesNotifier $subscriberChangesNotifier,
Validator $validator
Validator $validator,
SegmentsRepository $segmentsRepository,
EntityManager $entityManager
) {
$this->wp = $wp;
$this->welcomeScheduler = $welcomeScheduler;
@ -59,6 +71,10 @@ class WP {
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
$this->subscriberChangesNotifier = $subscriberChangesNotifier;
$this->validator = $validator;
$this->segmentsRepository = $segmentsRepository;
$this->entityManager = $entityManager;
$this->databaseConnection = $this->entityManager->getConnection();
$this->subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
}
/**
@ -69,46 +85,39 @@ class WP {
$wpUser = \get_userdata($wpUserId);
if ($wpUser === false) return;
$subscriber = Subscriber::where('wp_user_id', $wpUser->ID)
->findOne();
$subscriber = $this->subscribersRepository->findOneBy(['wpUserId' => $wpUserId]);
$currentFilter = $this->wp->currentFilter();
// Delete
if (in_array($currentFilter, ['delete_user', 'deleted_user', 'remove_user_from_blog'])) {
$this->deleteSubscriber($subscriber);
if ($subscriber instanceof SubscriberEntity) {
$this->deleteSubscriber($subscriber);
}
return;
}
$this->createOrUpdateSubscriber($currentFilter, $wpUser, $subscriber, $oldWpUserData);
$this->handleCreatingOrUpdatingSubscriber($currentFilter, $wpUser, $subscriber, $oldWpUserData);
}
/**
* @param false|Subscriber $subscriber
*
* @return void
*/
private function deleteSubscriber($subscriber) {
if ($subscriber !== false) {
// unlink subscriber from wp user and delete
$subscriber->set('wp_user_id', null);
$subscriber->delete();
}
private function deleteSubscriber(SubscriberEntity $subscriber): void {
$this->subscribersRepository->remove($subscriber);
$this->subscribersRepository->flush();
}
/**
* @param string $currentFilter
* @param \WP_User $wpUser
* @param Subscriber|false $subscriber
* @param ?SubscriberEntity $subscriber
* @param array|false $oldWpUserData
*/
private function createOrUpdateSubscriber(string $currentFilter, \WP_User $wpUser, $subscriber = false, $oldWpUserData = false): void {
private function handleCreatingOrUpdatingSubscriber(string $currentFilter, \WP_User $wpUser, ?SubscriberEntity $subscriber = null, $oldWpUserData = false): void {
// Add or update
$wpSegment = Segment::getWPSegment();
if (!$wpSegment) return;
$wpSegment = $this->segmentsRepository->getWPUsersSegment();
// find subscriber by email when is false
if (!$subscriber) {
$subscriber = Subscriber::where('email', $wpUser->user_email)->findOne(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
// find subscriber by email when is null
if (is_null($subscriber)) {
$subscriber = $this->subscribersRepository->findOneBy(['email' => $wpUser->user_email]); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
}
// get first name & last name
$firstName = html_entity_decode($wpUser->first_name); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$lastName = html_entity_decode($wpUser->last_name); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
@ -116,7 +125,7 @@ class WP {
$firstName = html_entity_decode($wpUser->display_name); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
}
$signupConfirmationEnabled = SettingsController::getInstance()->get('signup_confirmation.enabled');
$status = $signupConfirmationEnabled ? Subscriber::STATUS_UNCONFIRMED : Subscriber::STATUS_SUBSCRIBED;
$status = $signupConfirmationEnabled ? SubscriberEntity::STATUS_UNCONFIRMED : SubscriberEntity::STATUS_SUBSCRIBED;
// we want to mark a new subscriber as unsubscribe when the checkbox from registration is unchecked
if (isset($_POST['mailpoet']['subscribe_on_register_active']) && (bool)$_POST['mailpoet']['subscribe_on_register_active'] === true) {
$status = SubscriberEntity::STATUS_UNSUBSCRIBED;
@ -132,19 +141,18 @@ class WP {
'source' => Source::WORDPRESS_USER,
];
if ($subscriber !== false) {
$data['id'] = $subscriber->id();
if (!is_null($subscriber)) {
$data['id'] = $subscriber->getId();
unset($data['status']); // don't override status for existing users
unset($data['source']); // don't override status for existing users
}
$addingNewUserToDisabledWPSegment = $wpSegment->deletedAt !== null && $currentFilter === 'user_register';
$addingNewUserToDisabledWPSegment = $wpSegment->getDeletedAt() !== null && $currentFilter === 'user_register';
$otherActiveSegments = [];
if ($subscriber) {
$subscriber = $subscriber->withSegments();
$otherActiveSegments = array_filter($subscriber->segments ?? [], function ($segment) {
return $segment['type'] !== SegmentEntity::TYPE_WP_USERS && $segment['deleted_at'] === null;
$otherActiveSegments = array_filter($subscriber->getSegments()->toArray() ?? [], function (SegmentEntity $segment) {
return $segment->getType() !== SegmentEntity::TYPE_WP_USERS && $segment->getDeletedAt() === null;
});
}
$isWooCustomer = $this->wooHelper->isWooCommerceActive() && in_array('customer', $wpUser->roles, true);
@ -155,58 +163,86 @@ class WP {
$data['status'] = SubscriberEntity::STATUS_UNCONFIRMED;
}
$subscriber = Subscriber::createOrUpdate($data);
if ($subscriber->getErrors() === false && $subscriber->id > 0) {
// add subscriber to the WP Users segment
SubscriberSegment::subscribeToSegments(
$subscriber,
[$wpSegment->id]
);
try {
$subscriber = $this->createOrUpdateSubscriber($data, $subscriber);
} catch (\Exception $e) {
return; // fails silently as this was the behavior of this methods before the Doctrine refactor.
}
if (!$signupConfirmationEnabled && $subscriber->status === Subscriber::STATUS_SUBSCRIBED && $currentFilter === 'user_register') {
$subscriberSegment = $this->subscriberSegmentRepository->findOneBy([
'subscriber' => $subscriber->id(),
'segment' => $wpSegment->id(),
]);
// add subscriber to the WP Users segment
$this->subscriberSegmentRepository->subscribeToSegments(
$subscriber,
[$wpSegment]
);
if (!is_null($subscriberSegment)) {
$this->wp->doAction('mailpoet_segment_subscribed', $subscriberSegment);
}
}
if (!$signupConfirmationEnabled && $subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED && $currentFilter === 'user_register') {
$subscriberSegment = $this->subscriberSegmentRepository->findOneBy([
'subscriber' => $subscriber->getId(),
'segment' => $wpSegment->getId(),
]);
$subscribeOnRegisterEnabled = SettingsController::getInstance()->get('subscribe.on_register.enabled');
$sendConfirmationEmail =
$signupConfirmationEnabled
&& $subscribeOnRegisterEnabled
&& $currentFilter !== 'profile_update'
&& !$addingNewUserToDisabledWPSegment;
if ($sendConfirmationEmail && ($subscriber->status === Subscriber::STATUS_UNCONFIRMED)) {
/** @var ConfirmationEmailMailer $confirmationEmailMailer */
$confirmationEmailMailer = ContainerWrapper::getInstance()->get(ConfirmationEmailMailer::class);
$subscriberEntity = $this->subscribersRepository->findOneById($subscriber->id);
if ($subscriberEntity instanceof SubscriberEntity) {
try {
$confirmationEmailMailer->sendConfirmationEmailOnce($subscriberEntity);
} catch (\Exception $e) {
// ignore errors
}
}
}
// welcome email
$scheduleWelcomeNewsletter = false;
if (in_array($currentFilter, ['profile_update', 'user_register', 'add_user_role', 'set_user_role'])) {
$scheduleWelcomeNewsletter = true;
}
if ($scheduleWelcomeNewsletter === true) {
$this->welcomeScheduler->scheduleWPUserWelcomeNotification(
$subscriber->id,
(array)$wpUser,
(array)$oldWpUserData
);
if (!is_null($subscriberSegment)) {
$this->wp->doAction('mailpoet_segment_subscribed', $subscriberSegment);
}
}
$subscribeOnRegisterEnabled = SettingsController::getInstance()->get('subscribe.on_register.enabled');
$sendConfirmationEmail =
$signupConfirmationEnabled
&& $subscribeOnRegisterEnabled
&& $currentFilter !== 'profile_update'
&& !$addingNewUserToDisabledWPSegment;
if ($sendConfirmationEmail && ($subscriber->getStatus() === SubscriberEntity::STATUS_UNCONFIRMED)) {
/** @var ConfirmationEmailMailer $confirmationEmailMailer */
$confirmationEmailMailer = ContainerWrapper::getInstance()->get(ConfirmationEmailMailer::class);
try {
$confirmationEmailMailer->sendConfirmationEmailOnce($subscriber);
} catch (\Exception $e) {
// ignore errors
}
}
// welcome email
$scheduleWelcomeNewsletter = false;
if (in_array($currentFilter, ['profile_update', 'user_register', 'add_user_role', 'set_user_role'])) {
$scheduleWelcomeNewsletter = true;
}
if ($scheduleWelcomeNewsletter === true) {
$this->welcomeScheduler->scheduleWPUserWelcomeNotification(
$subscriber->getId(),
(array)$wpUser,
(array)$oldWpUserData
);
}
}
private function createOrUpdateSubscriber(array $data, ?SubscriberEntity $subscriber = null): SubscriberEntity {
if (is_null($subscriber)) {
$subscriber = new SubscriberEntity();
}
$subscriber->setWpUserId($data['wp_user_id']);
$subscriber->setEmail($data['email']);
$subscriber->setFirstName($data['first_name']);
$subscriber->setLastName($data['last_name']);
if (isset($data['status'])) {
$subscriber->setStatus($data['status']);
}
if (isset($data['source'])) {
$subscriber->setSource($data['source']);
}
if (isset($data['deleted_at'])) {
$subscriber->setDeletedAt($data['deleted_at']);
}
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
return $subscriber;
}
public function synchronizeUsers(): bool {
@ -227,6 +263,7 @@ class WP {
$this->insertUsersToSegment();
$this->removeOrphanedSubscribers();
$this->subscribersRepository->invalidateTotalSubscribersCache();
$this->subscribersRepository->refreshAll();
return true;
}
@ -241,32 +278,39 @@ class WP {
if (!$invalidWpUserIds) {
return;
}
ORM::for_table(Subscriber::$_table)->whereIn('wp_user_id', $invalidWpUserIds)->delete_many();
$this->subscribersRepository->removeByWpUserIds($invalidWpUserIds);
}
private function updateSubscribersEmails(): array {
global $wpdb;
Subscriber::rawExecute('SELECT NOW();');
$startTime = Subscriber::getLastStatement()->fetch(\PDO::FETCH_COLUMN);
$subscribersTable = Subscriber::$_table;
Subscriber::rawExecute(sprintf('
UPDATE IGNORE %1$s
INNER JOIN %2$s as wu ON %1$s.wp_user_id = wu.id
SET %1$s.email = wu.user_email;
', $subscribersTable, $wpdb->users));
$stmt = $this->databaseConnection->executeQuery('SELECT NOW();');
$startTime = $stmt->fetchOne();
return ORM::for_table(Subscriber::$_table)->raw_query(sprintf(
'SELECT wp_user_id as id, email FROM %s
WHERE updated_at >= \'%s\';
', $subscribersTable, $startTime))->findArray();
if (!is_string($startTime)) {
throw new \RuntimeException("Failed to fetch the current time.");
}
$updateSql =
"UPDATE IGNORE {$this->subscribersTable} s
INNER JOIN {$wpdb->users} as wu ON s.wp_user_id = wu.id
SET s.email = wu.user_email";
$this->databaseConnection->executeStatement($updateSql);
$selectSql =
"SELECT wp_user_id as id, email FROM {$this->subscribersTable}
WHERE updated_at >= '{$startTime}'";
$updatedEmails = $this->databaseConnection->fetchAllAssociative($selectSql);
return $updatedEmails;
}
private function insertSubscribers(): array {
global $wpdb;
$wpSegment = Segment::getWPSegment();
if (!$wpSegment) return [];
if ($wpSegment->deletedAt !== null) {
$wpSegment = $this->segmentsRepository->getWPUsersSegment();
if ($wpSegment->getDeletedAt() !== null) {
$subscriberStatus = SubscriberEntity::STATUS_UNCONFIRMED;
$deletedAt = 'CURRENT_TIMESTAMP()';
} else {
@ -274,92 +318,85 @@ class WP {
$subscriberStatus = $signupConfirmationEnabled ? SubscriberEntity::STATUS_UNCONFIRMED : SubscriberEntity::STATUS_SUBSCRIBED;
$deletedAt = 'null';
}
$subscribersTable = Subscriber::$_table;
$insertedUserIds = ORM::for_table($wpdb->users)->raw_query(sprintf(
'SELECT %2$s.id, %2$s.user_email as email FROM %2$s
LEFT JOIN %1$s AS mps ON mps.wp_user_id = %2$s.id
WHERE mps.wp_user_id IS NULL AND %2$s.user_email != ""
', $subscribersTable, $wpdb->users))->findArray();
Subscriber::rawExecute(sprintf(
'
INSERT IGNORE INTO %1$s(wp_user_id, email, status, created_at, `source`, deleted_at)
SELECT wu.id, wu.user_email, "%4$s", CURRENT_TIMESTAMP(), "%3$s", %5$s FROM %2$s wu
LEFT JOIN %1$s mps ON wu.id = mps.wp_user_id
WHERE mps.wp_user_id IS NULL AND wu.user_email != ""
ON DUPLICATE KEY UPDATE wp_user_id = wu.id
',
$subscribersTable,
$wpdb->users,
Source::WORDPRESS_USER,
$subscriberStatus,
$deletedAt
));
// Fetch users that are not in the subscribers table
$selectSql =
"SELECT u.id, u.user_email as email
FROM {$wpdb->users} u
LEFT JOIN {$this->subscribersTable} AS s ON s.wp_user_id = u.id
WHERE s.wp_user_id IS NULL AND u.user_email != ''";
$insertedUserIds = $this->databaseConnection->fetchAllAssociative($selectSql);
// Insert new users into the subscribers table
$insertSql =
"INSERT IGNORE INTO {$this->subscribersTable} (wp_user_id, email, status, created_at, `source`, deleted_at)
SELECT wu.id, wu.user_email, :subscriberStatus, CURRENT_TIMESTAMP(), :source, {$deletedAt}
FROM {$wpdb->users} wu
LEFT JOIN {$this->subscribersTable} s ON wu.id = s.wp_user_id
WHERE s.wp_user_id IS NULL AND wu.user_email != ''
ON DUPLICATE KEY UPDATE wp_user_id = wu.id";
$stmt = $this->databaseConnection->prepare($insertSql);
$stmt->bindValue('subscriberStatus', $subscriberStatus);
$stmt->bindValue('source', Source::WORDPRESS_USER);
$stmt->executeStatement();
return $insertedUserIds;
}
private function updateFirstNames(): void {
global $wpdb;
$subscribersTable = Subscriber::$_table;
Subscriber::rawExecute(sprintf('
UPDATE %1$s
JOIN %2$s as wpum ON %1$s.wp_user_id = wpum.user_id AND wpum.meta_key = "first_name"
SET %1$s.first_name = SUBSTRING(wpum.meta_value, 1, 255)
WHERE %1$s.first_name = ""
AND %1$s.wp_user_id IS NOT NULL
AND wpum.meta_value IS NOT NULL
', $subscribersTable, $wpdb->usermeta));
$sql =
"UPDATE {$this->subscribersTable} s
JOIN {$wpdb->usermeta} as wpum ON s.wp_user_id = wpum.user_id AND wpum.meta_key = 'first_name'
SET s.first_name = SUBSTRING(wpum.meta_value, 1, 255)
WHERE s.first_name = ''
AND s.wp_user_id IS NOT NULL
AND wpum.meta_value IS NOT NULL";
$this->databaseConnection->executeStatement($sql);
}
private function updateLastNames(): void {
global $wpdb;
$subscribersTable = Subscriber::$_table;
Subscriber::rawExecute(sprintf('
UPDATE %1$s
JOIN %2$s as wpum ON %1$s.wp_user_id = wpum.user_id AND wpum.meta_key = "last_name"
SET %1$s.last_name = SUBSTRING(wpum.meta_value, 1, 255)
WHERE %1$s.last_name = ""
AND %1$s.wp_user_id IS NOT NULL
AND wpum.meta_value IS NOT NULL
', $subscribersTable, $wpdb->usermeta));
$sql =
"UPDATE {$this->subscribersTable} s
JOIN {$wpdb->usermeta} as wpum ON s.wp_user_id = wpum.user_id AND wpum.meta_key = 'last_name'
SET s.last_name = SUBSTRING(wpum.meta_value, 1, 255)
WHERE s.last_name = ''
AND s.wp_user_id IS NOT NULL
AND wpum.meta_value IS NOT NULL";
$this->databaseConnection->executeStatement($sql);
}
private function updateFirstNameIfMissing(): void {
global $wpdb;
$subscribersTable = Subscriber::$_table;
Subscriber::rawExecute(sprintf('
UPDATE %1$s
JOIN %2$s wu ON %1$s.wp_user_id = wu.id
SET %1$s.first_name = wu.display_name
WHERE %1$s.first_name = ""
AND %1$s.wp_user_id IS NOT NULL
', $subscribersTable, $wpdb->users));
$sql =
"UPDATE {$this->subscribersTable} s
JOIN {$wpdb->users} wu ON s.wp_user_id = wu.id
SET s.first_name = wu.display_name
WHERE s.first_name = ''
AND s.wp_user_id IS NOT NULL";
$this->databaseConnection->executeStatement($sql);
}
private function insertUsersToSegment(): void {
$wpSegment = Segment::getWPSegment();
$subscribersTable = Subscriber::$_table;
$wpMailpoetSubscriberSegmentTable = SubscriberSegment::$_table;
Subscriber::rawExecute(sprintf('
INSERT IGNORE INTO %s(subscriber_id, segment_id, created_at)
SELECT mps.id, "%s", CURRENT_TIMESTAMP() FROM %s mps
WHERE mps.wp_user_id > 0
', $wpMailpoetSubscriberSegmentTable, $wpSegment->id, $subscribersTable));
$wpSegment = $this->segmentsRepository->getWPUsersSegment();
$subscribersSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
$sql =
"INSERT IGNORE INTO {$subscribersSegmentTable} (subscriber_id, segment_id, created_at)
SELECT s.id, '{$wpSegment->getId()}', CURRENT_TIMESTAMP() FROM {$this->subscribersTable} s
WHERE s.wp_user_id > 0";
$this->databaseConnection->executeStatement($sql);
}
private function removeOrphanedSubscribers(): void {
// remove orphaned wp segment subscribers (not having a matching wp user id),
// e.g. if wp users were deleted directly from the database
global $wpdb;
$wpSegment = Segment::getWPSegment();
$wpSegment->subscribers()
->leftOuterJoin($wpdb->users, [MP_SUBSCRIBERS_TABLE . '.wp_user_id', '=', 'wu.id'], 'wu')
->whereRaw('(wu.id IS NULL OR ' . MP_SUBSCRIBERS_TABLE . '.email = "")')
->findResultSet()
->set('wp_user_id', null)
->delete();
$this->subscribersRepository->removeOrphanedSubscribersFromWpSegment();
}
}

View File

@ -8,12 +8,11 @@ use MailPoet\Services\Bridge\API;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\License\Features\Subscribers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class AuthorizedSenderDomainController {
const OVERALL_STATUS_VERIFIED = 'verified';
const OVERALL_STATUS_PARTIALLY_VERIFIED = 'partially-verified';
const OVERALL_STATUS_UNVERIFIED = 'unverified';
const DOMAIN_STATUS_VERIFIED = 'verified';
const DOMAIN_STATUS_PARTIALLY_VERIFIED = 'partially-verified';
const DOMAIN_STATUS_UNVERIFIED = 'unverified';
const AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_CREATED = 'Sender domain exist';
const AUTHORIZED_SENDER_DOMAIN_ERROR_NOT_CREATED = 'Sender domain does not exist';
@ -22,8 +21,6 @@ class AuthorizedSenderDomainController {
const LOWER_LIMIT = 100;
const UPPER_LIMIT = 200;
const ENFORCEMENT_START_TIME = '2024-02-01 00:00:00 UTC';
const INSTALLED_AFTER_NEW_RESTRICTIONS_OPTION = 'installed_after_new_domain_restrictions';
const SENDER_DOMAINS_KEY = 'mailpoet_sender_domains';
@ -121,6 +118,7 @@ class AuthorizedSenderDomainController {
}
// Reset cached value since a new domain was added
$this->currentRecords = null;
$this->reloadCache();
return $response;
@ -150,7 +148,6 @@ class AuthorizedSenderDomainController {
throw new \InvalidArgumentException(self::AUTHORIZED_SENDER_DOMAIN_ERROR_NOT_CREATED);
}
$this->reloadCache();
$verifiedDomains = $this->getFullyVerifiedSenderDomains(true);
$alreadyVerified = in_array($domain, $verifiedDomains);
@ -166,6 +163,9 @@ class AuthorizedSenderDomainController {
throw new \InvalidArgumentException($response['message']);
}
$this->currentRecords = null;
$this->reloadCache();
return $response;
}
@ -179,7 +179,7 @@ class AuthorizedSenderDomainController {
* Returns sender domains that have all required records, including DMARC.
*/
public function getFullyVerifiedSenderDomains($domainsOnly = false): array {
$domainData = $this->getSenderDomainsByStatus([self::OVERALL_STATUS_VERIFIED]);
$domainData = $this->getSenderDomainsByStatus([self::DOMAIN_STATUS_VERIFIED]);
return $domainsOnly ? $this->extractDomains($domainData) : $domainData;
}
@ -187,17 +187,17 @@ class AuthorizedSenderDomainController {
* Returns sender domains that were verified before DMARC record was required.
*/
public function getPartiallyVerifiedSenderDomains($domainsOnly = false): array {
$domainData = $this->getSenderDomainsByStatus([self::OVERALL_STATUS_PARTIALLY_VERIFIED]);
$domainData = $this->getSenderDomainsByStatus([self::DOMAIN_STATUS_PARTIALLY_VERIFIED]);
return $domainsOnly ? $this->extractDomains($domainData) : $domainData;
}
public function getUnverifiedSenderDomains($domainsOnly = false): array {
$domainData = $this->getSenderDomainsByStatus([self::OVERALL_STATUS_UNVERIFIED]);
$domainData = $this->getSenderDomainsByStatus([self::DOMAIN_STATUS_UNVERIFIED]);
return $domainsOnly ? $this->extractDomains($domainData) : $domainData;
}
public function getFullyOrPartiallyVerifiedSenderDomains($domainsOnly = false): array {
$domainData = $this->getSenderDomainsByStatus([self::OVERALL_STATUS_PARTIALLY_VERIFIED,self::OVERALL_STATUS_VERIFIED]);
$domainData = $this->getSenderDomainsByStatus([self::DOMAIN_STATUS_PARTIALLY_VERIFIED,self::DOMAIN_STATUS_VERIFIED]);
return $domainsOnly ? $this->extractDomains($domainData) : $domainData;
}
@ -236,7 +236,6 @@ class AuthorizedSenderDomainController {
}
private function reloadCache() {
$this->currentRecords = null;
$this->currentRawData = $this->bridge->getRawSenderDomainData();
$this->wp->setTransient(self::SENDER_DOMAINS_KEY, $this->currentRawData, 60 * 60 * 24);
}
@ -261,11 +260,6 @@ class AuthorizedSenderDomainController {
return $this->currentRecords;
}
// TODO: Remove after the enforcement date has passed
public function isEnforcementOfNewRestrictionsInEffect(): bool {
return Carbon::now() >= Carbon::parse(self::ENFORCEMENT_START_TIME);
}
public function isNewUser(): bool {
$installedVersion = $this->settingsController->get('version');
@ -291,25 +285,12 @@ class AuthorizedSenderDomainController {
return $this->subscribers->getSubscribersCount() > self::UPPER_LIMIT;
}
private function restrictionsApply(): bool {
if ($this->settingsController->get('mta.method') !== Mailer::METHOD_MAILPOET) {
return false;
}
// TODO: Remove after the enforcement date has passed
if (!$this->isNewUser() && !$this->isEnforcementOfNewRestrictionsInEffect()) {
return false;
}
return true;
}
public function isAuthorizedDomainRequiredForNewCampaigns(): bool {
return $this->restrictionsApply() && !$this->isSmallSender();
return $this->settingsController->get('mta.method') === Mailer::METHOD_MAILPOET && !$this->isSmallSender();
}
public function isAuthorizedDomainRequiredForExistingCampaigns(): bool {
return $this->restrictionsApply() && $this->isBigSender();
return $this->settingsController->get('mta.method') === Mailer::METHOD_MAILPOET && $this->isBigSender();
}
public function getContextData(): array {
@ -319,9 +300,6 @@ class AuthorizedSenderDomainController {
'allSenderDomains' => $this->getAllSenderDomains(),
'senderRestrictions' => [
'lowerLimit' => self::LOWER_LIMIT,
'upperLimit' => self::UPPER_LIMIT,
'isNewUser' => $this->isNewUser(),
'isEnforcementOfNewRestrictionsInEffect' => $this->isEnforcementOfNewRestrictionsInEffect(),
'alwaysRewrite' => false,
],
];

View File

@ -89,4 +89,20 @@ class StatisticsClicksRepository extends Repository {
->getQuery()
->getResult();
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(StatisticsClickEntity::class, 's')
->where('s.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (StatisticsClickEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -7,6 +7,7 @@ use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoetVendor\Carbon\Carbon;
/**
* @extends Repository<StatisticsNewsletterEntity>
@ -29,7 +30,8 @@ class StatisticsNewslettersRepository extends Repository {
continue;
}
$entity = new StatisticsNewsletterEntity($newsletter, $queue, $subscriber);
$sentAt = Carbon::createFromTimestamp((int)current_time('timestamp'));
$entity = new StatisticsNewsletterEntity($newsletter, $queue, $subscriber, $sentAt);
$this->entityManager->persist($entity);
$entities[] = $entity;
@ -40,4 +42,20 @@ class StatisticsNewslettersRepository extends Repository {
$this->entityManager->flush();
}
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(StatisticsNewsletterEntity::class, 's')
->where('s.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (StatisticsNewsletterEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -90,4 +90,20 @@ class StatisticsOpensRepository extends Repository {
->orderBy('queue.newsletterRenderedSubject')
->setParameter('subscriber', $subscriber->getId());
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(StatisticsOpenEntity::class, 's')
->where('s.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (StatisticsOpenEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -112,4 +112,22 @@ class StatisticsWooCommercePurchasesRepository extends Repository {
}, $data);
return $data;
}
/** @param int[] $ids */
public function removeNewsletterDataByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->update(StatisticsWooCommercePurchaseEntity::class, 'swp')
->set('swp.newsletter', ':newsletter')
->where('swp.newsletter IN (:ids)')
->setParameter('newsletter', null)
->setParameter('ids', $ids)
->getQuery()
->execute();
// update was done via DQL, make sure the entities are also refreshed in the entity manager
$this->refreshAll(function (StatisticsWooCommercePurchaseEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}

View File

@ -178,23 +178,40 @@ class Import {
);
}
if ($existingSubscribers['data'] && $this->updateSubscribers) {
$updateExistingSubscribersStatus = false;
if ($existingSubscribers['data']) {
$allowedStatuses = [
SubscriberEntity::STATUS_SUBSCRIBED,
SubscriberEntity::STATUS_UNSUBSCRIBED,
SubscriberEntity::STATUS_INACTIVE,
];
if (in_array($this->existingSubscribersStatus, $allowedStatuses, true)) {
$updateExistingSubscribersStatus = true;
$existingSubscribers = $this->addField($existingSubscribers, 'status', $this->existingSubscribersStatus);
}
$updatedSubscribers =
$this->createOrUpdateSubscribers(
self::ACTION_UPDATE,
$existingSubscribers,
$this->subscribersCustomFields
);
if ($wpUsers) {
$this->synchronizeWPUsers($wpUsers);
if ($this->updateSubscribers) {
// Update existing subscribers' info (first_name, last_name etc.)
// as well as status (optionally) if the status column was added above
$updatedSubscribers =
$this->createOrUpdateSubscribers(
self::ACTION_UPDATE,
$existingSubscribers,
$this->subscribersCustomFields
);
if ($wpUsers) {
$this->synchronizeWPUsers($wpUsers);
}
} elseif ($updateExistingSubscribersStatus) {
// Only update existing subscribers' status
// For this we need to remove all other fields except email and status
$existingSubscribers['fields'] = array_intersect($existingSubscribers['fields'], ['email', 'status']);
$existingSubscribers['data'] = array_intersect_key($existingSubscribers['data'], array_flip(['email', 'status']));
$updatedSubscribers =
$this->createOrUpdateSubscribers(
self::ACTION_UPDATE,
$existingSubscribers
);
}
}
} catch (\Exception $e) {

View File

@ -10,6 +10,8 @@ use MailPoet\Entities\SubscriberCustomFieldEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Segments\DynamicSegments\FilterHandler;
use MailPoet\Subscribers\SubscriberCustomFieldRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoetVendor\Doctrine\DBAL\Connection;
use MailPoetVendor\Doctrine\DBAL\Driver\Statement;
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
@ -52,14 +54,24 @@ class ImportExportRepository {
/** @var FilterHandler */
private $filterHandler;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var SubscriberCustomFieldRepository */
private $subscriberCustomFieldRepository;
public function __construct(
EntityManager $entityManager,
SubscriberChangesNotifier $changesNotifier,
FilterHandler $filterHandler
FilterHandler $filterHandler,
SubscribersRepository $subscribersRepository,
SubscriberCustomFieldRepository $subscriberCustomFieldRepository
) {
$this->entityManager = $entityManager;
$this->subscriberChangesNotifier = $changesNotifier;
$this->filterHandler = $filterHandler;
$this->subscribersRepository = $subscribersRepository;
$this->subscriberCustomFieldRepository = $subscriberCustomFieldRepository;
}
/**
@ -183,6 +195,12 @@ class ImportExportRepository {
" . implode(' AND ', $keyColumnsConditions) . "
", $parameters, $parameterTypes);
$this->notifyUpdates($className, $columns, $data);
if ($className === SubscriberEntity::class) {
$this->subscribersRepository->refreshAll();
}
if ($className === SubscriberCustomFieldEntity::class) {
$this->subscriberCustomFieldRepository->refreshAll();
}
return $count;
}

View File

@ -12,6 +12,7 @@ use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Entities\SubscriberTagEntity;
use MailPoet\Entities\TagEntity;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Util\License\Features\Subscribers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
@ -38,14 +39,19 @@ class SubscribersRepository extends Repository {
/** @var SubscriberChangesNotifier */
private $changesNotifier;
/** @var SegmentsRepository */
private $segmentsRepository;
public function __construct(
EntityManager $entityManager,
SubscriberChangesNotifier $changesNotifier,
WPFunctions $wp
WPFunctions $wp,
SegmentsRepository $segmentsRepository
) {
$this->wp = $wp;
parent::__construct($entityManager);
$this->changesNotifier = $changesNotifier;
$this->segmentsRepository = $segmentsRepository;
}
protected function getEntityClassName() {
@ -553,6 +559,35 @@ class SubscribersRepository extends Repository {
return $count;
}
public function removeOrphanedSubscribersFromWpSegment(): void {
global $wpdb;
$segmentId = $this->segmentsRepository->getWpUsersSegment()->getId();
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$subscriberSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
$this->entityManager->getConnection()->executeStatement(
"DELETE s
FROM {$subscribersTable} s
INNER JOIN {$subscriberSegmentsTable} ss ON s.id = ss.subscriber_id
LEFT JOIN {$wpdb->users} u ON s.wp_user_id = u.id
WHERE ss.segment_id = :segmentId AND (u.id IS NULL OR s.email = '')",
['segmentId' => $segmentId], ['segmentId' => \PDO::PARAM_INT]
);
}
public function removeByWpUserIds(array $wpUserIds) {
$queryBuilder = $this->entityManager->createQueryBuilder();
$queryBuilder
->delete(SubscriberEntity::class, 's')
->where('s.wpUserId IN (:wpUserIds)')
->setParameter('wpUserIds', $wpUserIds);
return $queryBuilder->getQuery()->execute();
}
/**
* @return int - number of processed ids
*/

View File

@ -1,342 +0,0 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Tasks;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueAlias;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\InvalidStateException;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Models\ScheduledTask;
use MailPoet\Models\SendingQueue;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Util\Helpers;
use MailPoetVendor\Doctrine\ORM\ORMInvalidArgumentException;
/**
* A facade class containing all necessary models to work with a sending queue
* @property string|null $status
* @property int $taskId
* @property int $id
* @property int $newsletterId
* @property string $newsletterRenderedSubject
* @property string|array $newsletterRenderedBody
* @property bool $nonExistentColumn
* @property string $scheduledAt
* @property int $priority
*/
class Sending {
const TASK_TYPE = SendingQueueAlias::TASK_TYPE;
/** @var ScheduledTask */
private $task;
/** @var SendingQueue */
private $queue;
private $queueFields = [
'id',
'task_id',
'newsletter_id',
'newsletter_rendered_subject',
'newsletter_rendered_body',
'count_total',
'count_processed',
'count_to_process',
'meta',
];
private $commonFields = [
'created_at',
'updated_at',
'deleted_at',
];
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
/** @var ScheduledTaskEntity */
private $scheduledTaskEntity;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
private function __construct(
ScheduledTask $task = null,
SendingQueue $queue = null
) {
if (!$task instanceof ScheduledTask) {
/** @var ScheduledTask $task */
$task = ScheduledTask::create();
$task->type = self::TASK_TYPE;
$task->save();
}
if (!$queue instanceof SendingQueue) {
/** @var SendingQueue $queue */
$queue = SendingQueue::create();
$queue->newsletterId = 0;
$queue->taskId = $task->id;
$queue->save();
}
if ($task->type !== self::TASK_TYPE) {
throw new \Exception('Only tasks of type "' . self::TASK_TYPE . '" are accepted by this class');
}
$this->task = $task;
$this->queue = $queue;
$this->scheduledTaskSubscribersRepository = ContainerWrapper::getInstance()->get(ScheduledTaskSubscribersRepository::class);
$this->scheduledTasksRepository = ContainerWrapper::getInstance()->get(ScheduledTasksRepository::class);
$this->sendingQueuesRepository = ContainerWrapper::getInstance()->get(SendingQueuesRepository::class);
// needed to make sure that the task has an ID so that we can retrieve the ScheduledTaskEntity while this class still uses Paris
$this->save();
$scheduledTaskEntity = $this->scheduledTasksRepository->findOneById($this->task->id);
if (!$scheduledTaskEntity instanceof ScheduledTaskEntity) {
throw new InvalidStateException('Scheduled task entity not found');
}
$this->scheduledTaskEntity = $scheduledTaskEntity;
}
public static function create(ScheduledTask $task = null, SendingQueue $queue = null) {
return new self($task, $queue);
}
public static function createManyFromTasks($tasks) {
if (empty($tasks)) {
return [];
}
$tasksIds = array_map(function($task) {
return $task->id;
}, $tasks);
$queues = SendingQueue::whereIn('task_id', $tasksIds)->findMany();
$queuesIndex = [];
foreach ($queues as $queue) {
$queuesIndex[$queue->taskId] = $queue;
}
$result = [];
foreach ($tasks as $task) {
if (!empty($queuesIndex[$task->id])) {
$result[] = self::create($task, $queuesIndex[$task->id]);
} else {
static::handleInvalidTask($task);
}
}
return $result;
}
public static function handleInvalidTask(ScheduledTask $task) {
$loggerFactory = LoggerFactory::getInstance();
$loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->error(
'invalid sending task found',
['task_id' => $task->id]
);
$task->status = ScheduledTask::STATUS_INVALID;
$task->save();
}
public static function createFromScheduledTask(ScheduledTask $task) {
$queue = SendingQueue::where('task_id', $task->id)->findOne();
if (!$queue) {
return false;
}
return self::create($task, $queue);
}
public static function createFromQueue(SendingQueue $queue) {
$task = $queue->task()->findOne();
if (!$task) {
return false;
}
return self::create($task, $queue);
}
public static function getByNewsletterId($newsletterId) {
$queue = SendingQueue::where('newsletter_id', $newsletterId)
->orderByDesc('updated_at')
->findOne();
if (!$queue instanceof SendingQueue) {
return false;
}
return self::createFromQueue($queue);
}
public function asArray() {
$queue = array_intersect_key(
$this->queue->asArray(),
array_flip($this->queueFields)
);
$task = $this->task->asArray();
return array_merge($task, $queue);
}
public function getErrors() {
$queueErrors = $this->queue->getErrors();
$taskErrors = $this->task->getErrors();
if (empty($queueErrors) && empty($taskErrors)) {
return false;
}
return array_merge((array)$queueErrors, (array)$taskErrors);
}
public function save() {
$this->queue->save();
$this->task->save();
$errors = $this->getErrors();
if ($errors) {
$loggerFactory = LoggerFactory::getInstance();
$loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->error(
'error saving sending task',
['task_id' => $this->task->id, 'queue_id' => $this->queue->id, 'errors' => $errors]
);
}
return $this;
}
public function delete() {
$this->scheduledTaskSubscribersRepository->deleteByScheduledTask($this->scheduledTaskEntity);
$this->scheduledTasksRepository->remove($this->scheduledTaskEntity);
try {
$sendingQueueEntity = $this->scheduledTaskEntity->getSendingQueue();
if ($sendingQueueEntity) {
$this->sendingQueuesRepository->remove($sendingQueueEntity);
}
} catch (ORMInvalidArgumentException $e) {
// This entity can already be removed. E.g. in the NewslettersRepository when deleting newsletters in bulk
;
}
$this->scheduledTasksRepository->flush();
}
public function queue() {
return $this->queue;
}
public function task() {
return $this->task;
}
public function getSubscribers($processed = null) {
if (is_null($processed)) {
$subscribers = $this->scheduledTaskSubscribersRepository->findBy(['task' => $this->task->id]);
} else if ($processed) {
$subscribers = $this->scheduledTaskSubscribersRepository->findBy(
['task' => $this->task->id, 'processed' => ScheduledTaskSubscriberEntity::STATUS_PROCESSED]
);
} else {
$subscribers = $this->scheduledTaskSubscribersRepository->findBy(
['task' => $this->task->id, 'processed' => ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED]
);
}
return array_map(
function(ScheduledTaskSubscriberEntity $scheduledTaskSubscriber) {
return (string)$scheduledTaskSubscriber->getSubscriberId();
},
$subscribers
);
}
public function setSubscribers(array $subscriberIds) {
$this->scheduledTaskSubscribersRepository->setSubscribers($this->scheduledTaskEntity, $subscriberIds);
$this->updateCount();
}
public function updateProcessedSubscribers(array $processedSubscribers): bool {
$this->scheduledTaskSubscribersRepository->updateProcessedSubscribers($this->scheduledTaskEntity, $processedSubscribers);
$this->scheduledTasksRepository->refresh($this->scheduledTaskEntity); // needed while Sending still uses Paris
$this->status = $this->scheduledTaskEntity->getStatus();
return $this->updateCount(count($processedSubscribers))->getErrors() === false;
}
public function updateCount(?int $count = null) {
if ($count) {
// increment/decrement counts based on known subscriber count, don't exceed the bounds
$this->queue->countProcessed = min($this->queue->countProcessed + $count, $this->queue->countTotal);
$this->queue->countToProcess = max($this->queue->countToProcess - $count, 0);
} else {
// query DB to update counts, slower but more accurate, to be used if count isn't known
$this->queue->countProcessed = $this->scheduledTaskSubscribersRepository->countProcessed($this->scheduledTaskEntity);
$this->queue->countToProcess = $this->scheduledTaskSubscribersRepository->countUnprocessed($this->scheduledTaskEntity);
$this->queue->countTotal = $this->queue->countProcessed + $this->queue->countToProcess;
}
return $this->queue->save();
}
public function hydrate(array $data) {
foreach ($data as $k => $v) {
$this->__set($k, $v);
}
}
public function validate() {
return $this->queue->validate() && $this->task->validate();
}
public function getMeta() {
return $this->queue->getMeta();
}
public function __isset($prop) {
$prop = Helpers::camelCaseToUnderscore($prop);
if ($this->isQueueProperty($prop)) {
return isset($this->queue->$prop);
} else {
return isset($this->task->$prop);
}
}
public function __get($prop) {
$prop = Helpers::camelCaseToUnderscore($prop);
if ($this->isQueueProperty($prop)) {
return $this->queue->$prop;
} else {
return $this->task->$prop;
}
}
public function __set($prop, $value) {
$prop = Helpers::camelCaseToUnderscore($prop);
if ($this->isCommonProperty($prop)) {
$this->queue->$prop = $value;
$this->task->$prop = $value;
} elseif ($this->isQueueProperty($prop)) {
$this->queue->$prop = $value;
} else {
$this->task->$prop = $value;
}
}
public function __call($name, $args) {
$obj = method_exists($this->queue, $name) ? $this->queue : $this->task;
$callback = [$obj, $name];
if (is_callable($callback)) {
return call_user_func_array($callback, $args);
}
}
private function isQueueProperty($prop) {
return in_array($prop, $this->queueFields);
}
private function isCommonProperty($prop) {
return in_array($prop, $this->commonFields);
}
}

View File

@ -82,9 +82,6 @@ class SenderDomainAuthenticationNotices {
}
public function isErrorStyle(): bool {
if (!$this->authorizedSenderDomainController->isEnforcementOfNewRestrictionsInEffect()) {
return false;
}
if (
$this->subscribersFeatures->getSubscribersCount() < AuthorizedSenderDomainController::UPPER_LIMIT
|| $this->isPartiallyVerified()
@ -100,17 +97,6 @@ class SenderDomainAuthenticationNotices {
}
public function getNoticeContentForFreeMailUsers(int $contactCount): string {
if (!$this->authorizedSenderDomainController->isEnforcementOfNewRestrictionsInEffect()) {
// translators: %1$s is the domain of the user's default from address, %2$s is a rewritten version of their default from address, %3$s is HTML for an 'update sender' button, and %4$s is HTML for a Learn More button
return sprintf(__("<strong>Update your sender email address to a branded domain by February 1st, 2024 to continue sending your campaigns.</strong>
<span>Starting on February 1st, 2024, MailPoet will no longer be able to send from email addresses on shared 3rd party domains like <strong>%1\$s</strong>. Please change your campaigns to send from an email address on your site's branded domain. Your emails will temporarily be sent from <strong>%2\$s</strong>.</span> <p>%3\$s &nbsp; %4\$s</p>", 'mailpoet'),
"@" . $this->getDefaultFromDomain(),
$this->authorizedSenderDomainController->getRewrittenEmailAddress($this->getDefaultFromAddress()),
$this->getUpdateSenderButton(),
$this->getLearnMoreAboutFreeMailButton()
);
}
if ($contactCount <= AuthorizedSenderDomainController::UPPER_LIMIT) {
// translators: %1$s is the domain of the user's default from address, %2$s is a rewritten version of their default from address, %3$s is HTML for an 'update sender' button, and %4$s is HTML for a Learn More button
return sprintf(__("<strong>Update your sender email address to a branded domain to continue sending your campaigns.</strong>
@ -133,7 +119,7 @@ class SenderDomainAuthenticationNotices {
}
public function getNoticeContentForBrandedDomainUsers(bool $isPartiallyVerified, int $contactCount): string {
if (!$this->authorizedSenderDomainController->isEnforcementOfNewRestrictionsInEffect() || $isPartiallyVerified || $contactCount <= AuthorizedSenderDomainController::LOWER_LIMIT) {
if ($isPartiallyVerified || $contactCount <= AuthorizedSenderDomainController::LOWER_LIMIT) {
// translators: %1$s is HTML for an 'authenticate domain' button, %2$s is HTML for a Learn More button
return sprintf(__("<strong>Authenticate your sender domain to improve email delivery rates.</strong>
<span>Major mailbox providers require you to authenticate your sender domain to confirm you sent the emails, and may place unauthenticated emails in the “Spam” folder. Please authenticate your sender domain to ensure your marketing campaigns are compliant and will reach your contacts.</span><p>%1\$s &nbsp; %2\$s</p>", 'mailpoet'),

View File

@ -50,13 +50,13 @@ class Tracker {
}
/**
* @param array<int, array{revenue: float, campaign_id: string, campaign_type: string, orders_count: int}> $campaignsData
* @param array<int, array{revenue: float, campaign_id: string|null, campaign_type: string, orders_count: int}> $campaignsData
* @return array<string, string|int|float>
*/
private function formatCampaignsData(array $campaignsData): array {
return array_reduce($campaignsData, function($result, array $campaign): array {
$newsletter = $this->newslettersRepository->findOneById((int)$campaign['campaign_id']);
$keyPrefix = 'campaign_' . $campaign['campaign_id'];
$keyPrefix = 'campaign_' . ($campaign['campaign_id'] ?? 0);
$result[$keyPrefix . '_revenue'] = $campaign['revenue'];
$result[$keyPrefix . '_orders_count'] = $campaign['orders_count'];
$result[$keyPrefix . '_type'] = $campaign['campaign_type'];

View File

@ -2,7 +2,7 @@
/*
* Plugin Name: MailPoet
* Version: 4.42.0
* Version: 4.43.1
* 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' => '4.42.0',
'version' => '4.43.1',
'filename' => __FILE__,
'path' => dirname(__FILE__),
'autoloader' => dirname(__FILE__) . '/vendor/autoload.php',

View File

@ -17,14 +17,9 @@
"prepare": "cd .. && husky install"
},
"lint-staged": {
"*.{scss,css}": "pnpm run stylelint",
"*.{js,jsx,ts,tsx}": "eslint --max-warnings 0",
"*.php": [
"phplint",
"./do qa:code-sniffer",
"./do qa:minimal-plugin-standard",
"bash -c './do qa:phpstan'"
]
"*.{scss,css}": "tasks/lint-staged-css.sh",
"*.{js,jsx,ts,tsx}": "tasks/lint-staged-js.sh",
"*.php": "tasks/lint-staged-php.sh"
},
"dependencies": {
"@babel/runtime": "^7.22.11",

View File

@ -3,7 +3,7 @@ Contributors: mailpoet, woocommerce, automattic
Tags: email, email marketing, post notification, woocommerce emails, email automation, newsletter, newsletter builder, newsletter subscribers
Requires at least: 6.3
Tested up to: 6.4
Stable tag: 4.42.0
Stable tag: 4.43.1
Requires PHP: 7.4
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -228,6 +228,25 @@ Check our [Knowledge Base](https://kb.mailpoet.com) or contact us through our [s
== Changelog ==
= 4.43.1 - 2024-02-12 =
* Fixed: automation and legacy emails not rendering if one has an invalid option;
* Fixed: sending tasks incorrectly marked as invalid;
* Fixed: tutorial button on email templates and send email pages.
= 4.43.0 - 2024-02-05 =
* Improved: during import, "Update existing subscriber status" no longer requires "Update existing subscriber info" to be enabled;
* Fixed: error "EntityManager is Closed" during sending on MySQL 8;
* Fixed: error "new entity was found through the relationship" during email sending;
* Fixed: Some strings where not translateable;
* Fixed: errors when sending some emails;
* Fixed: automation listing trash and delete action UI behavior.
= 4.42.1 - 2024-01-30 =
* Improved: Display sender domain authentication notices in Automations;
* Improved: Added Domain Authentication to the onboarding tasks;
* Fixed: Post notification emails can become stuck;
* Fixed: Some legacy automatic emails where not visible in the new automations listing page.
= 4.42.0 - 2024-01-22 =
* Updated: minimum required WooCommerce version to 8.4;
* Improved: Made it obvious that list names may be visible to subscribers;

View File

@ -0,0 +1,16 @@
#!/bin/sh
set -e
source $PWD/.env
if [ "$MP_GIT_HOOKS_ENABLE" != "true" ]; then
echo "MP_GIT_HOOKS_ENABLE is not set to 'true', skipping lint-staged-css."
exit 0
fi
if [ "$MP_GIT_HOOKS_STYLELINT" = "true" ]; then
pnpm run stylelint $@
else
echo "MP_GIT_HOOKS_STYLELINT is not set to 'true', skipping stylelint."
fi

View File

@ -0,0 +1,16 @@
#!/bin/sh
set -e
source $PWD/.env
if [ "$MP_GIT_HOOKS_ENABLE" != "true" ]; then
echo "MP_GIT_HOOKS_ENABLE is not set to 'true', skipping lint-staged-js"
exit 0
fi
if [ "$MP_GIT_HOOKS_ESLINT" = "true" ]; then
eslint --max-warnings 0 $@
else
echo "MP_GIT_HOOKS_ESLINT is not set to 'true', skipping eslint"
fi

View File

@ -0,0 +1,35 @@
#!/bin/sh
set -e
source $PWD/.env
if [ "$MP_GIT_HOOKS_ENABLE" != "true" ]; then
echo "MP_GIT_HOOKS_ENABLE is not set to 'true'. Skipping lint-staged-php."
exit 0
fi
if [ "$MP_GIT_HOOKS_PHPLINT" = "true" ]; then
phplint $@
else
echo "MP_GIT_HOOKS_PHPLINT not set to 'true', skipping phplint"
fi
if [ "$MP_GIT_HOOKS_CODE_SNIFFER" = "true" ]; then
./do qa:code-sniffer $@
else
echo "MP_GIT_HOOKS_CODE_SNIFFER not set to 'true', skipping code sniffer"
fi
if [ "$MP_GIT_HOOKS_MINIMAL_PLUGIN_STANDARDS" = "true" ]; then
./do qa:minimal-plugin-standard $@
else
echo "MP_GIT_HOOKS_MINIMAL_PLUGIN_STANDARDS not set to 'true', skipping minimal plugin standards"
fi
if [ "$MP_GIT_HOOKS_PHPSTAN" = "true" ]; then
bash -c './do qa:phpstan' $@
else
echo "MP_GIT_HOOKS_PHPSTAN not set to 'true', skipping PHPStan"
fi

View File

@ -580,11 +580,6 @@ parameters:
count: 1
path: ../../lib/Segments/SegmentsSimpleListRepository.php
-
message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
count: 1
path: ../../lib/Segments/WP.php
-
message: "#^Cannot cast mixed to int\\.$#"
count: 1

View File

@ -570,11 +570,6 @@ parameters:
count: 1
path: ../../lib/Segments/SegmentsSimpleListRepository.php
-
message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
count: 1
path: ../../lib/Segments/WP.php
-
message: "#^Cannot cast mixed to int\\.$#"
count: 1

View File

@ -83,6 +83,19 @@ class IntegrationTester extends \Codeception\Actor {
return $userId;
}
/**
* Deletes a WP user directly from the database without triggering any hooks.
* Needed to be able to test deleting orphaned subscribers.
*/
public function deleteWPUserFromDatabase(int $id): void {
global $wpdb;
$this->entityManager->getConnection()->executeStatement(
"DELETE FROM {$wpdb->users} WHERE id = :id",
['id' => $id], ['id' => \PDO::PARAM_INT]
);
}
public function createWordPressTerm(string $term, string $taxonomy, array $args = []): int {
$term = wp_insert_term($term, $taxonomy, $args);
if ($term instanceof WP_Error) {

View File

@ -9,12 +9,13 @@ use MailPoet\API\JSON\Response as APIResponse;
use MailPoet\API\JSON\ResponseBuilders\NewslettersResponseBuilder;
use MailPoet\API\JSON\v1\Newsletters;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\NewsletterSegmentEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Logging\LogRepository;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Preview\SendPreviewController;
@ -29,9 +30,10 @@ use MailPoet\Router\Router;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Settings\SettingsController;
use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\Test\DataFactories\Newsletter;
use MailPoet\Test\DataFactories\NewsletterOption;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
use MailPoet\Test\DataFactories\SendingQueue as SendingQueueFactory;
use MailPoet\Util\License\Features\Subscribers;
use MailPoet\WooCommerce\Helper as WCHelper;
use MailPoet\WP\Emoji;
@ -201,13 +203,7 @@ class NewslettersTest extends \MailPoetTest {
verify($response->status)->equals(APIResponse::STATUS_OK);
$updatedNewsletter = $this->newsletterRepository->findOneById($this->newsletter->getId());
$this->assertInstanceOf(NewsletterEntity::class, $updatedNewsletter); // PHPStan
verify($response->data)->equals(
$this->newslettersResponseBuilder->build($updatedNewsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
])
);
verify($response->data)->equals($this->newslettersResponseBuilder->build($updatedNewsletter, [NewslettersResponseBuilder::RELATION_SEGMENTS]));
verify($updatedNewsletter->getType())->equals('Updated type');
verify($updatedNewsletter->getSubject())->equals('Updated subject');
verify($updatedNewsletter->getPreheader())->equals('Updated preheader');
@ -290,23 +286,32 @@ class NewslettersTest extends \MailPoetTest {
public function testItReschedulesPastDuePostNotificationsWhenStatusIsSetBackToActive() {
$schedule = sprintf('0 %d * * *', Carbon::createFromTimestamp(WPFunctions::get()->currentTime('timestamp'))->hour); // every day at current hour
$randomFutureDate = Carbon::createFromTimestamp(WPFunctions::get()->currentTime('timestamp'))->addDays(10)->format('Y-m-d H:i:s'); // 10 days from now
$randomFutureDate = Carbon::createFromTimestamp(WPFunctions::get()->currentTime('timestamp'))->addDays(10); // 10 days from now
(new NewsletterOption())->create($this->postNotification, NewsletterOptionFieldEntity::NAME_SCHEDULE, $schedule);
$sendingQueue1 = SendingTask::create();
$sendingQueue1->newsletterId = $this->postNotification->getId();
$sendingQueue1->scheduledAt = $this->scheduler->getPreviousRunDate($schedule);
$sendingQueue1->status = SendingQueueEntity::STATUS_SCHEDULED;
$sendingQueue1->save();
$sendingQueue2 = SendingTask::create();
$sendingQueue2->newsletterId = $this->postNotification->getId();
$sendingQueue2->scheduledAt = $randomFutureDate;
$sendingQueue2->status = SendingQueueEntity::STATUS_SCHEDULED;
$sendingQueue2->save();
$sendingQueue3 = SendingTask::create();
$sendingQueue3->newsletterId = $this->postNotification->getId();
$sendingQueue3->scheduledAt = $this->scheduler->getPreviousRunDate($schedule);
$sendingQueue3->save();
$scheduledTask1 = (new ScheduledTaskFactory())
->create(
SendingQueueWorker::TASK_TYPE,
ScheduledTaskEntity::STATUS_SCHEDULED,
new Carbon($this->scheduler->getPreviousRunDate($schedule))
);
(new SendingQueueFactory())->create($scheduledTask1, $this->postNotification);
$scheduledTask2 = (new ScheduledTaskFactory())
->create(
SendingQueueWorker::TASK_TYPE,
ScheduledTaskEntity::STATUS_SCHEDULED,
$randomFutureDate
);
(new SendingQueueFactory())->create($scheduledTask2, $this->postNotification);
$scheduledTask3 = (new ScheduledTaskFactory())
->create(
SendingQueueWorker::TASK_TYPE,
null,
new Carbon($this->scheduler->getPreviousRunDate($schedule))
);
(new SendingQueueFactory())->create($scheduledTask3, $this->postNotification);
$this->entityManager->clear();
$this->endpoint->setStatus(
@ -321,7 +326,7 @@ class NewslettersTest extends \MailPoetTest {
verify($tasks[0]->getScheduledAt()->format('Y-m-d H:i:s'))->equals($this->scheduler->getNextRunDate($schedule));
// future scheduled notifications are left intact
$this->assertInstanceOf(\DateTimeInterface::class, $tasks[1]->getScheduledAt());
verify($tasks[1]->getScheduledAt()->format('Y-m-d H:i:s'))->equals($randomFutureDate);
verify($tasks[1]->getScheduledAt())->equals($randomFutureDate);
// previously unscheduled (e.g., sent/sending) notifications are left intact
$this->assertInstanceOf(\DateTimeInterface::class, $tasks[2]->getScheduledAt());
verify($tasks[2]->getScheduledAt()->format('Y-m-d H:i:s'))->equals($this->scheduler->getPreviousRunDate($schedule));
@ -353,13 +358,7 @@ class NewslettersTest extends \MailPoetTest {
verify($response->status)->equals(APIResponse::STATUS_OK);
$newsletter = $this->newsletterRepository->findOneById($this->newsletter->getId());
$this->assertInstanceOf(NewsletterEntity::class, $newsletter);
verify($response->data)->equals(
$this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
])
);
verify($response->data)->equals($this->newslettersResponseBuilder->build($newsletter));
verify($response->data['deleted_at'])->null();
verify($response->meta['count'])->equals(1);
}
@ -369,13 +368,7 @@ class NewslettersTest extends \MailPoetTest {
verify($response->status)->equals(APIResponse::STATUS_OK);
$newsletter = $this->newsletterRepository->findOneById($this->newsletter->getId());
$this->assertInstanceOf(NewsletterEntity::class, $newsletter);
verify($response->data)->equals(
$this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
])
);
verify($response->data)->equals($this->newslettersResponseBuilder->build($newsletter));
verify($response->data['deleted_at'])->notNull();
verify($response->meta['count'])->equals(1);
}
@ -400,13 +393,7 @@ class NewslettersTest extends \MailPoetTest {
verify($response->status)->equals(APIResponse::STATUS_OK);
$newsletterCopy = $this->newsletterRepository->findOneBy(['subject' => 'Copy of My Standard Newsletter']);
$this->assertInstanceOf(NewsletterEntity::class, $newsletterCopy);
verify($response->data)->equals(
$this->newslettersResponseBuilder->build($newsletterCopy, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
])
);
verify($response->data)->equals($this->newslettersResponseBuilder->build($newsletterCopy));
verify($response->meta['count'])->equals(1);
$hookName = 'mailpoet_api_newsletters_duplicate_after';
@ -417,13 +404,7 @@ class NewslettersTest extends \MailPoetTest {
verify($response->status)->equals(APIResponse::STATUS_OK);
$newsletterCopy = $this->newsletterRepository->findOneBy(['subject' => 'Copy of My Post Notification']);
$this->assertInstanceOf(NewsletterEntity::class, $newsletterCopy);
verify($response->data)->equals(
$this->newslettersResponseBuilder->build($newsletterCopy, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
])
);
verify($response->data)->equals($this->newslettersResponseBuilder->build($newsletterCopy));
verify($response->meta['count'])->equals(1);
}

View File

@ -8,11 +8,16 @@ use MailPoet\API\JSON\v1\SendingQueue as SendingQueueAPI;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Newsletter\NewsletterValidator;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Test\DataFactories\Newsletter;
use MailPoet\Test\DataFactories\Newsletter as NewsletterFactory;
use MailPoet\Test\DataFactories\NewsletterOption;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
use MailPoet\Test\DataFactories\Segment as SegmentFactory;
use MailPoet\Test\DataFactories\SendingQueue as SendingQueueFactory;
use MailPoet\Test\DataFactories\Subscriber as SubscriberFactory;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
class SendingQueueTest extends \MailPoetTest {
@ -26,7 +31,7 @@ class SendingQueueTest extends \MailPoetTest {
parent::_before();
$this->newsletterOptionsFactory = new NewsletterOption();
$this->newsletter = (new Newsletter())
$this->newsletter = (new NewsletterFactory())
->withSubject('My Standard Newsletter')
->withDefaultBody()
->create();
@ -48,16 +53,38 @@ class SendingQueueTest extends \MailPoetTest {
];
$this->newsletterOptionsFactory->createMultipleOptions($newsletter, $newsletterOptions);
$sendingQueue = $this->diContainer->get(SendingQueueAPI::class);
$result = $sendingQueue->add(['newsletter_id' => $newsletter->getId()]);
$repo = $this->diContainer->get(ScheduledTasksRepository::class);
$scheduledTask = $repo->findOneById($result->data['task_id']);
$sendingQueueApi = $this->diContainer->get(SendingQueueAPI::class);
$result = $sendingQueueApi->add(['newsletter_id' => $newsletter->getId()]);
$sendingQueue = $newsletter->getLatestQueue();
$this->assertInstanceOf(SendingQueueEntity::class, $sendingQueue);
$scheduledTask = $sendingQueue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $scheduledTask);
verify($scheduledTask->getStatus())->equals(ScheduledTaskEntity::STATUS_SCHEDULED);
$scheduled = $scheduledTask->getScheduledAt();
$this->assertInstanceOf(\DateTimeInterface::class, $scheduled);
verify($scheduled->format('Y-m-d H:i:s'))->equals($newsletterOptions['scheduledAt']);
verify($scheduledTask->getType())->equals(SendingQueue::TASK_TYPE);
$this->assertSame($sendingQueue->getId(), $result->data['id']);
$this->assertSame(SendingQueue::TASK_TYPE, $result->data['type']);
$this->assertSame(ScheduledTaskEntity::STATUS_SCHEDULED, $result->data['status']);
$this->assertSame(5, $result->data['priority']);
$this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $result->data['scheduled_at']);
$this->assertNull($result->data['processed_at']);
$this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $result->data['created_at']);
$this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $result->data['updated_at']);
$this->assertNull($result->data['deleted_at']);
$this->assertNull($result->data['in_progress']);
$this->assertSame(0, $result->data['reschedule_count']);
$this->assertNull($result->data['meta']);
$this->assertSame($scheduledTask->getId(), $result->data['task_id']);
$this->assertSame($newsletter->getId(), $result->data['newsletter_id']);
$this->assertNull($result->data['newsletter_rendered_body']);
$this->assertNull($result->data['newsletter_rendered_subject']);
$this->assertSame(0, $result->data['count_total']);
$this->assertSame(0, $result->data['count_processed']);
$this->assertSame(0, $result->data['count_to_process']);
$this->assertSame(200, $result->status);
}
public function testItReturnsErrorIfSubscribersLimitReached() {
@ -110,6 +137,67 @@ class SendingQueueTest extends \MailPoetTest {
verify($scheduled->format('Y-m-d H:i:s'))->equals('2018-11-11 11:00:00');
}
public function testAddReturnsErrorIfThereAreNoSubscribersAssociatedWithTheNewsletter() {
$sendingQueue = $this->diContainer->get(SendingQueueAPI::class);
$expectedResult = [
'errors' => [
[
'error' => 'unknown',
'message' => 'There are no subscribers in that list!',
],
],
];
$data = $sendingQueue->add(['newsletter_id' => $this->newsletter->getId()])->getData();
$this->assertSame($expectedResult, $data);
}
public function testAddChangesNewsletterStatus() {
$sendingQueueApi = $this->diContainer->get(SendingQueueAPI::class);
$segment = (new SegmentFactory())->create();
$subscriber = (new SubscriberFactory())->withSegments([$segment])->create();
$newsletter = (new NewsletterFactory())
->withSegments([$segment])
->withSubscriber($subscriber)
->create();
$this->assertSame(NewsletterEntity::STATUS_DRAFT, $newsletter->getStatus());
$result = $sendingQueueApi->add(['newsletter_id' => $newsletter->getId()]);
$sendingQueue = $newsletter->getLatestQueue();
$this->assertInstanceOf(SendingQueueEntity::class, $sendingQueue);
$scheduledTask = $sendingQueue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $scheduledTask);
$this->assertSame(NewsletterEntity::STATUS_SENDING, $newsletter->getStatus());
$this->assertSame(1, $sendingQueue->getCountTotal());
$this->assertSame(0, $sendingQueue->getCountProcessed());
$this->assertSame(1, $sendingQueue->getCountToProcess());
$this->assertNull($scheduledTask->getStatus());
$this->assertNull($scheduledTask->getScheduledAt());
$this->assertSame($sendingQueue->getId(), $result->data['id']);
$this->assertSame(SendingQueue::TASK_TYPE, $result->data['type']);
$this->assertNull($result->data['status']);
$this->assertSame(5, $result->data['priority']);
$this->assertNull($result->data['scheduled_at']);
$this->assertNull($result->data['processed_at']);
$this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $result->data['created_at']);
$this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $result->data['updated_at']);
$this->assertNull($result->data['deleted_at']);
$this->assertNull($result->data['in_progress']);
$this->assertSame(0, $result->data['reschedule_count']);
$this->assertNull($result->data['meta']);
$this->assertSame($scheduledTask->getId(), $result->data['task_id']);
$this->assertSame($newsletter->getId(), $result->data['newsletter_id']);
$this->assertNull($result->data['newsletter_rendered_body']);
$this->assertNull($result->data['newsletter_rendered_subject']);
$this->assertSame(1, $result->data['count_total']);
$this->assertSame(0, $result->data['count_processed']);
$this->assertSame(1, $result->data['count_to_process']);
$this->assertSame(200, $result->status);
}
public function testItRejectsInvalidNewsletters() {
$sendingQueue = $this->getServiceWithOverrides(SendingQueueAPI::class, [
'newsletterValidator' => Stub::make(NewsletterValidator::class, ['validate' => 'some error']),
@ -120,4 +208,24 @@ class SendingQueueTest extends \MailPoetTest {
verify($response['errors'][0]['message'])->stringContainsString('some error');
verify($response['errors'][0]['error'])->stringContainsString('bad_request');
}
public function testAddReturnsErrorIfNewsletterIsAlreadyBeingSent() {
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueue::TASK_TYPE, null);
(new SendingQueueFactory())->create($scheduledTask, $this->newsletter);
$expectedResult = [
'errors' => [
[
'error' => 'not_found',
'message' => 'This newsletter is already being sent.',
],
],
];
$sendingQueue = $this->diContainer->get(SendingQueueAPI::class);
$data = $sendingQueue->add(['newsletter_id' => $this->newsletter->getId()])->getData();
$this->assertSame($expectedResult, $data);
}
}

View File

@ -11,7 +11,6 @@ use MailPoet\Cron\DaemonHttpRunner;
use MailPoet\Cron\Triggers\WordPress;
use MailPoet\Cron\Workers\SimpleWorker;
use MailPoet\Cron\Workers\WorkersFactory;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Settings\SettingsController;
use MailPoet\WP\Functions as WPFunctions;
@ -82,7 +81,10 @@ class DaemonHttpRunnerTest extends \MailPoetTest {
}
});
$daemon = new Daemon($this->cronHelper, $cronWorkerRunnerMock, $this->createWorkersFactoryMock(), $this->diContainer->get(LoggerFactory::class));
$daemon = $this->getServiceWithOverrides(Daemon::class, [
'cronWorkerRunner' => $cronWorkerRunnerMock,
'workersFactory' => $this->createWorkersFactoryMock(),
]);
$daemonHttpRunner = $this->make(DaemonHttpRunner::class, [
'pauseExecution' => null,
'callSelf' => null,
@ -208,7 +210,10 @@ class DaemonHttpRunnerTest extends \MailPoetTest {
$cronWorkerRunner = $this->make(CronWorkerRunner::class, [
'run' => null,
]);
$daemon = new Daemon($this->cronHelper, $cronWorkerRunner, $this->createWorkersFactoryMock(), $this->diContainer->get(LoggerFactory::class));
$daemon = $this->getServiceWithOverrides(Daemon::class, [
'cronWorkerRunner' => $cronWorkerRunner,
'workersFactory' => $this->createWorkersFactoryMock(),
]);
$daemonHttpRunner->__construct($daemon, $this->cronHelper, SettingsController::getInstance(), $this->diContainer->get(WordPress::class));
$daemonHttpRunner->run($data);
$updatedDaemon = $this->settings->get(CronHelper::DAEMON_SETTING);
@ -228,7 +233,10 @@ class DaemonHttpRunnerTest extends \MailPoetTest {
throw new \Exception();
}
});
$daemon = new Daemon($this->cronHelper, $cronWorkerRunnerMock, $this->createWorkersFactoryMock(), $this->diContainer->get(LoggerFactory::class));
$daemon = $this->getServiceWithOverrides(Daemon::class, [
'cronWorkerRunner' => $cronWorkerRunnerMock,
'workersFactory' => $this->createWorkersFactoryMock(),
]);
$daemonHttpRunner = $this->make(DaemonHttpRunner::class, [
'pauseExecution' => null,
'callSelf' => null,
@ -264,7 +272,10 @@ class DaemonHttpRunnerTest extends \MailPoetTest {
$cronWorkerRunnerMock = $this->make(CronWorkerRunner::class, [
'run' => null,
]);
$daemon = new Daemon($this->cronHelper, $cronWorkerRunnerMock, $this->createWorkersFactoryMock(), $this->diContainer->get(LoggerFactory::class));
$daemon = $this->getServiceWithOverrides(Daemon::class, [
'cronWorkerRunner' => $cronWorkerRunnerMock,
'workersFactory' => $this->createWorkersFactoryMock(),
]);
$daemonHttpRunner->__construct($daemon, $this->cronHelper, SettingsController::getInstance(), $this->diContainer->get(WordPress::class));
$daemonHttpRunner->run($data);
verify(ignore_user_abort())->equals(true);

View File

@ -9,15 +9,11 @@ use MailPoet\Cron\Daemon;
use MailPoet\Cron\Workers\SimpleWorker;
use MailPoet\Cron\Workers\WorkersFactory;
use MailPoet\Entities\LogEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Logging\LogRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\WP\Functions as WpFunctions;
class DaemonTest extends \MailPoetTest {
/** @var CronHelper */
private $cronHelper;
/** @var SettingsController */
private $settings;
@ -30,7 +26,6 @@ class DaemonTest extends \MailPoetTest {
public function _before() {
parent::_before();
$this->settings = SettingsController::getInstance();
$this->cronHelper = $this->diContainer->get(CronHelper::class);
$this->logRepository = $this->diContainer->get(LogRepository::class);
$this->wp = $this->diContainer->get(WpFunctions::class);
}
@ -43,7 +38,10 @@ class DaemonTest extends \MailPoetTest {
'token' => 123,
];
$this->settings->set(CronHelper::DAEMON_SETTING, $data);
$daemon = new Daemon($this->cronHelper, $cronWorkerRunner, $this->createWorkersFactoryMock(), $this->diContainer->get(LoggerFactory::class));
$daemon = $this->getServiceWithOverrides(Daemon::class, [
'cronWorkerRunner' => $cronWorkerRunner,
'workersFactory' => $this->createWorkersFactoryMock(),
]);
$daemon->run($data);
}
@ -57,7 +55,10 @@ class DaemonTest extends \MailPoetTest {
'token' => 123,
];
$this->settings->set(CronHelper::DAEMON_SETTING, $data);
$daemon = new Daemon($this->cronHelper, $cronWorkerRunner, $this->createWorkersFactoryMock(), $this->diContainer->get(LoggerFactory::class));
$daemon = $this->getServiceWithOverrides(Daemon::class, [
'cronWorkerRunner' => $cronWorkerRunner,
'workersFactory' => $this->createWorkersFactoryMock(),
]);
$daemon->run($data);
$log = $this->logRepository->findOneBy(['name' => 'cron', 'level' => 400]);
$this->assertInstanceOf(LogEntity::class, $log);
@ -82,7 +83,10 @@ class DaemonTest extends \MailPoetTest {
'createScheduleWorker' => function () {throw new \Exception('createScheduleWorker should not be called');
},
]);
$daemon = new Daemon($this->cronHelper, $cronWorkerRunner, $factoryMock, $this->diContainer->get(LoggerFactory::class));
$daemon = $this->getServiceWithOverrides(Daemon::class, [
'cronWorkerRunner' => $cronWorkerRunner,
'workersFactory' => $factoryMock,
]);
$daemon->run($data);
$log = $this->logRepository->findOneBy(['name' => 'cron', 'level' => 400]);
verify($log)->null();

View File

@ -1395,6 +1395,30 @@ class SendingQueueTest extends \MailPoetTest {
$this->assertSame(false, $this->scheduledTask->getInProgress());
}
public function testItCanProcessPostNotificationHistoryWithoutPosts() {
// Cleanup data from self::before
$this->truncateEntity(NewsletterEntity::class);
$this->truncateEntity(SendingQueueEntity::class);
$this->truncateEntity(ScheduledTaskEntity::class);
$this->entityManager->clear();
// Prepare post notification history for sending
// The body added via createNewsletter doesn't contain ALC block so there will be no posts inserted and the newsletter will be deleted
$parentNewsletter = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION, 'Post Notification', NewsletterEntity::STATUS_ACTIVE);
$newsletter = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, 'To Delete', NewsletterEntity::STATUS_SENDING);
$newsletter->setParent($parentNewsletter);
$queue = $this->createQueueWithTask($newsletter);
$sendingQueueWorker = $this->getSendingQueueWorker();
$sendingQueueWorker->process();
$task = $queue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $task);
// Re-fetch newsletter, task and queue from DB and verify they were deleted
verify($this->entityManager->find(ScheduledTaskEntity::class, $task->getId()))->null();
verify($this->entityManager->find(SendingQueueEntity::class, $queue->getId()))->null();
verify($this->entityManager->find(NewsletterEntity::class, $newsletter->getId()))->null();
}
private function createNewsletter(string $type, $subject, string $status = NewsletterEntity::STATUS_DRAFT): NewsletterEntity {
$newsletter = new NewsletterEntity();
$newsletter->setType($type);

View File

@ -26,6 +26,7 @@ use MailPoet\Newsletter\Renderer\Blocks\Coupon;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Router\Router;
use MailPoet\RuntimeException;
use MailPoet\Test\DataFactories\DynamicSegment;
use MailPoet\Test\DataFactories\Newsletter as NewsletterFactory;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
@ -308,7 +309,9 @@ class NewsletterTest extends \MailPoetTest {
public function testItDoesNotRenderSubscriberShortcodeInSubjectWhenPreprocessingNewsletter() {
$this->newsletter->setSubject('Newsletter for [subscriber:firstname] [date:dordinal]');
$this->newslettersRepository->persist($this->newsletter);
$this->newsletter = $this->newsletterTask->preProcessNewsletter($this->newsletter, $this->scheduledTaskEntity);
$newsletter = $this->newsletterTask->preProcessNewsletter($this->newsletter, $this->scheduledTaskEntity);
$this->assertInstanceOf(NewsletterEntity::class, $newsletter);
$this->newsletter = $newsletter;
$sendingQueue = $this->sendingQueuesRepository->findOneBy(['newsletter' => $this->newsletter]);
$this->assertInstanceOf(SendingQueueEntity::class, $sendingQueue);
@ -320,7 +323,9 @@ class NewsletterTest extends \MailPoetTest {
public function testItUsesADefaultSubjectIfRenderedSubjectIsEmptyWhenPreprocessingNewsletter() {
$this->newsletter->setSubject(' [custom_shortcode:should_render_empty] ');
$this->newslettersRepository->persist($this->newsletter);
$this->newsletter = $this->newsletterTask->preProcessNewsletter($this->newsletter, $this->scheduledTaskEntity);
$newsletter = $this->newsletterTask->preProcessNewsletter($this->newsletter, $this->scheduledTaskEntity);
$this->assertInstanceOf(NewsletterEntity::class, $newsletter);
$this->newsletter = $newsletter;
$sendingQueue = $this->sendingQueuesRepository->findOneBy(['newsletter' => $this->newsletter]);
$this->assertInstanceOf(SendingQueueEntity::class, $sendingQueue);
@ -357,8 +362,7 @@ class NewsletterTest extends \MailPoetTest {
}
public function testItRendersShortcodesAndReplacesSubscriberDataInLinks() {
$newsletter = $this->newsletterTask->preProcessNewsletter($this->newsletter, $this->scheduledTaskEntity);
$newsletterEntity = $this->newslettersRepository->findOneById($newsletter->getId());
$newsletterEntity = $this->newsletterTask->preProcessNewsletter($this->newsletter, $this->scheduledTaskEntity);
$this->assertInstanceOf(NewsletterEntity::class, $newsletterEntity);
$result = $this->newsletterTask->prepareNewsletterForSending(
$newsletterEntity,
@ -375,8 +379,7 @@ class NewsletterTest extends \MailPoetTest {
public function testItDoesNotReplaceSubscriberDataInLinksWhenTrackingIsNotEnabled() {
$newsletterTask = $this->newsletterTask;
$newsletterTask->trackingEnabled = false;
$newsletter = $newsletterTask->preProcessNewsletter($this->newsletter, $this->scheduledTaskEntity);
$newsletterEntity = $this->newslettersRepository->findOneById($newsletter->getId());
$newsletterEntity = $newsletterTask->preProcessNewsletter($this->newsletter, $this->scheduledTaskEntity);
$this->assertInstanceOf(NewsletterEntity::class, $newsletterEntity);
$result = $newsletterTask->prepareNewsletterForSending(
$newsletterEntity,
@ -455,8 +458,7 @@ class NewsletterTest extends \MailPoetTest {
$this->sendingQueueEntity->setNewsletter($newsletter);
$this->sendingQueuesRepository->persist($this->sendingQueueEntity);
$newsletter = $newsletterTask->preProcessNewsletter($newsletter, $this->scheduledTaskEntity);
$newsletterEntity = $this->newslettersRepository->findOneById($newsletter->getId());
$newsletterEntity = $newsletterTask->preProcessNewsletter($newsletter, $this->scheduledTaskEntity);
$this->assertInstanceOf(NewsletterEntity::class, $newsletterEntity);
$result = $newsletterTask->prepareNewsletterForSending(
$newsletterEntity,
@ -618,4 +620,18 @@ class NewsletterTest extends \MailPoetTest {
$this->assertNull($this->scheduledTasksRepository->findOneById($scheduledTaskId));
$this->assertNull($this->sendingQueuesRepository->findOneById($sendingQueueId));
}
public function testItThrowsExceptionWhenTaskHasNoQueue(): void {
$scheduledTask = new ScheduledTaskEntity();
$this->entityManager->persist($scheduledTask);
$newsletter = (new NewsletterFactory())
->withType(NewsletterEntity::TYPE_STANDARD)
->withStatus(NewsletterEntity::STATUS_SENDING)
->withSubject(Fixtures::get('newsletter_subject_template'))
->withBody(json_decode(Fixtures::get('newsletter_body_template'), true))
->create();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Cant pre-process newsletter without queue.');
$this->newsletterTask->preProcessNewsletter($newsletter, $scheduledTask);
}
}

View File

@ -56,6 +56,80 @@ class RepositoryTest extends \MailPoetTest {
$this->assertSame($this->getEntityFromIdentityMap($setting3->getId()), $setting3);
}
public function testItCanRefreshAll(): void {
$repository = $this->createRepository();
$setting1 = $this->createSetting('name-1', 'value-1');
$setting2 = $this->createSetting('name-2', 'value-2');
$setting3 = $this->createSetting('name-3', 'value-3');
$this->entityManager->createQueryBuilder()
->update(SettingEntity::class, 's')
->set('s.value', ':value')
->where('s.name = :name')
->setParameter('value', 'new-value-1')
->setParameter('name', 'name-1')
->getQuery()
->execute();
$this->entityManager->createQueryBuilder()
->update(SettingEntity::class, 's')
->set('s.value', ':value')
->where('s.name = :name')
->setParameter('value', 'new-value-2')
->setParameter('name', 'name-2')
->getQuery()
->execute();
$this->assertSame($setting1->getValue(), 'value-1');
$this->assertSame($setting2->getValue(), 'value-2');
$this->assertSame($setting3->getValue(), 'value-3');
$repository->refreshAll();
$this->assertSame($setting1->getValue(), 'new-value-1');
$this->assertSame($setting2->getValue(), 'new-value-2');
$this->assertSame($setting3->getValue(), 'value-3');
}
public function testItCanRefreshSelectively(): void {
$repository = $this->createRepository();
$setting1 = $this->createSetting('name-1', 'value-1');
$setting2 = $this->createSetting('name-2', 'value-2');
$setting3 = $this->createSetting('name-3', 'value-3');
$this->entityManager->createQueryBuilder()
->update(SettingEntity::class, 's')
->set('s.value', ':value')
->where('s.name = :name')
->setParameter('value', 'new-value-1')
->setParameter('name', 'name-1')
->getQuery()
->execute();
$this->entityManager->createQueryBuilder()
->update(SettingEntity::class, 's')
->set('s.value', ':value')
->where('s.name = :name')
->setParameter('value', 'new-value-2')
->setParameter('name', 'name-2')
->getQuery()
->execute();
$this->assertSame($setting1->getValue(), 'value-1');
$this->assertSame($setting2->getValue(), 'value-2');
$this->assertSame($setting3->getValue(), 'value-3');
$repository->refreshAll(function (SettingEntity $setting) {
return in_array($setting->getName(), ['name-1', 'name-3'], true);
});
$this->assertSame($setting1->getValue(), 'new-value-1');
$this->assertSame($setting2->getValue(), 'value-2');
$this->assertSame($setting3->getValue(), 'value-3');
}
private function createSetting(string $name, string $value): SettingEntity {
$setting = new SettingEntity();
$setting->setName($name);

View File

@ -79,6 +79,27 @@ class MailerTest extends \MailPoetTest {
'email' => 'test@email.com',
])
)->equals('First Last <test@email.com>');
$subscriber = (new SubscriberFactory())
->withFirstName('First')
->withLastName('Last')
->withEmail('test1@email.com')
->create();
verify($mailer->formatSubscriberNameAndEmailAddress($subscriber))
->equals('First Last <test1@email.com>');
$subscriber = (new SubscriberFactory())
->withEmail('test2@email.com')
->create();
verify($mailer->formatSubscriberNameAndEmailAddress($subscriber))
->equals('test2@email.com');
$subscriber = (new SubscriberFactory())
->withLastName('Last')
->withEmail('test3@email.com')
->create();
verify($mailer->formatSubscriberNameAndEmailAddress($subscriber))
->equals('Last <test3@email.com>');
}
public function testItCanSend() {

View File

@ -0,0 +1,61 @@
<?php declare(strict_types = 1);
namespace MailPoet\Migrations\App;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Test\DataFactories\Newsletter as NewsletterFactory;
//phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
class Migration_20240202_130053_App_Test extends \MailPoetTest {
/** @var Migration_20240202_130053_App */
private $migration;
public function _before() {
parent::_before();
$this->migration = new Migration_20240202_130053_App($this->diContainer);
}
public function testItMigratesIncorrectlyMarkedSentNewslettersAsSent() {
$incorrectStandardNewsletter = (new NewsletterFactory())
->withStatus(NewsletterEntity::STATUS_SENDING)
->withSendingQueue(['status' => ScheduledTaskEntity::STATUS_COMPLETED])
->create();
$incorrectPostNotification = (new NewsletterFactory())
->withStatus(NewsletterEntity::STATUS_SENDING)
->withType(NewsletterEntity::TYPE_NOTIFICATION_HISTORY)
->withSendingQueue(['status' => ScheduledTaskEntity::STATUS_COMPLETED])
->create();
$correctlySendingNewsletter = (new NewsletterFactory())
->withStatus(NewsletterEntity::STATUS_SENDING)
->withSendingQueue(['status' => null]) // Running task
->create();
$correctlySentNewsletter = (new NewsletterFactory())
->withStatus(NewsletterEntity::STATUS_SENT)
->withSendingQueue(['status' => ScheduledTaskEntity::STATUS_COMPLETED])
->create();
$draftNewsletter = (new NewsletterFactory())
->withStatus(NewsletterEntity::STATUS_DRAFT)
->create();
$welcomeEmailNewsletter = (new NewsletterFactory())
->withStatus(NewsletterEntity::STATUS_ACTIVE)
->withSendingQueue(['status' => ScheduledTaskEntity::STATUS_COMPLETED])
->create();
$this->migration->run();
$this->entityManager->refresh($incorrectStandardNewsletter);
$this->entityManager->refresh($incorrectPostNotification);
$this->entityManager->refresh($correctlySendingNewsletter);
$this->entityManager->refresh($correctlySentNewsletter);
$this->entityManager->refresh($draftNewsletter);
$this->entityManager->refresh($welcomeEmailNewsletter);
verify($incorrectStandardNewsletter->getStatus())->equals(NewsletterEntity::STATUS_SENT);
verify($incorrectPostNotification->getStatus())->equals(NewsletterEntity::STATUS_SENT);
verify($correctlySendingNewsletter->getStatus())->equals(NewsletterEntity::STATUS_SENDING);
verify($correctlySentNewsletter->getStatus())->equals(NewsletterEntity::STATUS_SENT);
verify($draftNewsletter->getStatus())->equals(NewsletterEntity::STATUS_DRAFT);
verify($welcomeEmailNewsletter->getStatus())->equals(NewsletterEntity::STATUS_ACTIVE);
}
}

View File

@ -0,0 +1,327 @@
<?php declare(strict_types = 1);
namespace integration\Migrations\App;
use DateTimeImmutable;
use DateTimeInterface;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Migrations\App\Migration_20240207_105912_App;
use MailPoet\Statistics\StatisticsNewslettersRepository;
use MailPoet\Test\DataFactories\Newsletter as NewsletterFactory;
use MailPoet\Test\DataFactories\ScheduledTask;
use MailPoet\Test\DataFactories\ScheduledTaskSubscriber;
use MailPoet\Test\DataFactories\SendingQueue;
use MailPoet\Test\DataFactories\Subscriber;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
class Migration_20240207_105912_App_Test extends \MailPoetTest {
/** @var Migration_20240207_105912_App */
private $migration;
public function _before() {
parent::_before();
$this->migration = new Migration_20240207_105912_App($this->diContainer);
}
public function testItPausesInvalidTasksWithUnprocessedSubscribers(): void {
$newsletter = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SENDING);
$task = $this->createTask($newsletter, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
]);
$this->migration->run();
$this->refreshAll([$newsletter, $task]);
$this->assertSame(NewsletterEntity::STATUS_SENDING, $newsletter->getStatus());
$this->assertSame(ScheduledTaskEntity::STATUS_PAUSED, $task->getStatus());
}
public function testItCompletesInvalidTasksWithAllProcessedSubscribers(): void {
$newsletter = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SENDING);
$task = $this->createTask($newsletter, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 0,
'failedSubscribers' => 1,
]);
$this->migration->run();
$this->refreshAll([$newsletter, $task]);
$this->assertSame(NewsletterEntity::STATUS_SENT, $newsletter->getStatus());
$this->assertEquals($this->getMaxUpdatedDateFromTaskSubscribers($task), $newsletter->getSentAt());
$this->assertSame(ScheduledTaskEntity::STATUS_COMPLETED, $task->getStatus());
}
public function testItIgnoresNonSendingNewsletters(): void {
$newsletter1 = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SCHEDULED);
$task1 = $this->createTask($newsletter1, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
]);
$newsletter2 = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_ACTIVE);
$task2 = $this->createTask($newsletter1, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
]);
$this->migration->run();
$this->refreshAll([$newsletter1, $task1, $newsletter2, $task2]);
$this->assertSame(NewsletterEntity::STATUS_SCHEDULED, $newsletter1->getStatus());
$this->assertNull($newsletter1->getSentAt());
$this->assertSame(ScheduledTaskEntity::STATUS_INVALID, $task1->getStatus());
$this->assertSame(NewsletterEntity::STATUS_ACTIVE, $newsletter2->getStatus());
$this->assertNull($newsletter2->getSentAt());
$this->assertSame(ScheduledTaskEntity::STATUS_INVALID, $task2->getStatus());
}
public function testItIgnoresNonCampaignNewsletters(): void {
$newsletter = $this->createNewsletter(NewsletterEntity::TYPE_WELCOME, NewsletterEntity::STATUS_SENDING);
$task = $this->createTask($newsletter, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
]);
$this->migration->run();
$this->refreshAll([$newsletter, $task]);
$this->assertSame(NewsletterEntity::STATUS_SENDING, $newsletter->getStatus());
$this->assertNull($newsletter->getSentAt());
$this->assertSame(ScheduledTaskEntity::STATUS_INVALID, $task->getStatus());
}
public function testItIgnoresDeletedTasks(): void {
$newsletter = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SENDING);
$task = $this->createTask($newsletter, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
'isDeleted' => true,
]);
$this->migration->run();
$this->refreshAll([$newsletter, $task]);
$this->assertSame(NewsletterEntity::STATUS_SENDING, $newsletter->getStatus());
$this->assertNull($newsletter->getSentAt());
$this->assertSame(ScheduledTaskEntity::STATUS_INVALID, $task->getStatus());
$this->assertNotNull($task->getDeletedAt());
}
public function testItIgnoresDeletedNewsletters(): void {
$newsletter = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SENDING, true);
$task = $this->createTask($newsletter, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
]);
$this->migration->run();
$this->refreshAll([$newsletter, $task]);
$this->assertSame(NewsletterEntity::STATUS_SENDING, $newsletter->getStatus());
$this->assertNull($newsletter->getSentAt());
$this->assertSame(ScheduledTaskEntity::STATUS_INVALID, $task->getStatus());
}
public function testMultipleNewslettersAtOnce(): void {
// invalid task with unprocessed subscribers
$newsletter1 = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SENDING);
$task1 = $this->createTask($newsletter1, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
]);
// invalid task with all subscribers processed
$newsletter2 = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SENDING);
$task2 = $this->createTask($newsletter2, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 0,
'failedSubscribers' => 1,
]);
// invalid task with non-sending newsletter
$newsletter3 = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SCHEDULED);
$task3 = $this->createTask($newsletter3, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
]);
// invalid task with non-campaign newsletter
$newsletter4 = $this->createNewsletter(NewsletterEntity::TYPE_WELCOME, NewsletterEntity::STATUS_SENDING);
$task4 = $this->createTask($newsletter4, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 1,
'failedSubscribers' => 1,
]);
$this->migration->run();
$this->refreshAll([$newsletter1, $task1, $newsletter2, $task2, $newsletter3, $task3, $newsletter4, $task4]);
$this->assertSame(NewsletterEntity::STATUS_SENDING, $newsletter1->getStatus());
$this->assertSame(ScheduledTaskEntity::STATUS_PAUSED, $task1->getStatus());
$this->assertNull($newsletter1->getSentAt());
$this->assertSame(NewsletterEntity::STATUS_SENT, $newsletter2->getStatus());
$this->assertSame(ScheduledTaskEntity::STATUS_COMPLETED, $task2->getStatus());
$this->assertEquals($this->getMaxUpdatedDateFromTaskSubscribers($task2), $newsletter2->getSentAt());
$this->assertSame(NewsletterEntity::STATUS_SCHEDULED, $newsletter3->getStatus());
$this->assertSame(ScheduledTaskEntity::STATUS_INVALID, $task3->getStatus());
$this->assertNull($newsletter3->getSentAt());
$this->assertSame(NewsletterEntity::STATUS_SENDING, $newsletter4->getStatus());
$this->assertSame(ScheduledTaskEntity::STATUS_INVALID, $task4->getStatus());
$this->assertNull($newsletter4->getSentAt());
}
public function testItUpdatesCountsWhenCompletingInvalidTasks(): void {
$newsletter = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SENDING);
$task = $this->createTask($newsletter, [
'status' => ScheduledTaskEntity::STATUS_INVALID,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 0,
'failedSubscribers' => 1,
]);
$queue = $task->getSendingQueue();
$this->assertNotNull($queue);
// incorrect processed/to-process counts
$queue->setCountProcessed(0);
$queue->setCountToProcess(4);
$queue->setCountTotal(4);
$this->entityManager->flush();
$this->migration->run();
$this->refreshAll([$newsletter, $task, $queue]);
$this->assertSame(NewsletterEntity::STATUS_SENT, $newsletter->getStatus());
$this->assertEquals($this->getMaxUpdatedDateFromTaskSubscribers($task), $newsletter->getSentAt());
$this->assertSame(ScheduledTaskEntity::STATUS_COMPLETED, $task->getStatus());
// counts should be correct now
$this->assertSame(4, $queue->getCountProcessed());
$this->assertSame(0, $queue->getCountToProcess());
$this->assertSame(4, $queue->getCountTotal());
}
public function testItBackfillsMissingNewsletterStatistics(): void {
$newsletter = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SENT);
$task = $this->createTask($newsletter, [
'status' => ScheduledTaskEntity::STATUS_COMPLETED,
'processedSubscribers' => 3,
'unprocessedSubscribers' => 2,
'failedSubscribers' => 1,
]);
$repository = $this->diContainer->get(StatisticsNewslettersRepository::class);
$this->assertCount(0, $repository->findAll());
$this->assertNull($newsletter->getSentAt());
$this->migration->run();
$this->refreshAll([$newsletter, $task]);
$this->assertSame(NewsletterEntity::STATUS_SENT, $newsletter->getStatus());
$this->assertEquals($this->getMaxUpdatedDateFromTaskSubscribers($task), $newsletter->getSentAt());
$this->assertSame(ScheduledTaskEntity::STATUS_COMPLETED, $task->getStatus());
$stats = $repository->findAll();
$processedSubscribers = $task->getSubscribersByProcessed(ScheduledTaskSubscriberEntity::STATUS_PROCESSED);
$this->assertCount(4, $stats); // 3 ok + 1 failed
$this->assertCount(4, $processedSubscribers); // 3 ok + 1 failed
for ($i = 0; $i < 4; $i++) {
$this->assertSame($newsletter, $stats[$i]->getNewsletter());
$this->assertSame($task->getSendingQueue(), $stats[$i]->getQueue());
$subscriberIndex = array_search($stats[$i]->getSubscriber(), $processedSubscribers, true);
$subscriber = $processedSubscribers[$subscriberIndex] ?? null;
$this->assertNotNull($subscriber);
$this->assertSame($subscriber, $stats[$i]->getSubscriber());
$this->assertEquals($subscriber->getUpdatedAt(), $stats[$i]->getSentAt());
}
}
private function createNewsletter(string $newsletterType, string $newsletterStatus, bool $isDeleted = false): NewsletterEntity {
$newsletterFactory = (new NewsletterFactory())
->withType($newsletterType)
->withStatus($newsletterStatus);
if ($isDeleted) {
$newsletterFactory->withDeleted();
}
return $newsletterFactory->create();
}
/**
* @param NewsletterEntity $newsletter
* @param array{
* status?: string,
* processedSubscribers?: int,
* unprocessedSubscribers?: int,
* failedSubscribers?: int,
* } $params
*/
private function createTask(NewsletterEntity $newsletter, array $params = []): ScheduledTaskEntity {
$taskStatus = $params['status'] ?? ScheduledTaskEntity::STATUS_INVALID;
$processedSubscribers = $params['processedSubscribers'] ?? 0;
$unprocessedSubscribers = $params['unprocessedSubscribers'] ?? 0;
$failedSubscribers = $params['failedSubscribers'] ?? 0;
$isDeleted = $params['isDeleted'] ?? false;
$task = (new ScheduledTask())->create(SendingQueueWorker::TASK_TYPE, $taskStatus);
if ($isDeleted) {
$task->setDeletedAt(new DateTimeImmutable());
$this->entityManager->flush();
}
for ($i = 0; $i < $processedSubscribers; $i++) {
(new ScheduledTaskSubscriber())->createProcessed($task, (new Subscriber())->create());
}
for ($i = 0; $i < $unprocessedSubscribers; $i++) {
(new ScheduledTaskSubscriber())->createUnprocessed($task, (new Subscriber())->create());
}
for ($i = 0; $i < $failedSubscribers; $i++) {
(new ScheduledTaskSubscriber())->createFailed($task, (new Subscriber())->create());
}
(new SendingQueue())->create($task, $newsletter);
return $task;
}
private function refreshAll(array $entities) {
foreach ($entities as $entity) {
$this->entityManager->refresh($entity);
}
}
private function getMaxUpdatedDateFromTaskSubscribers(ScheduledTaskEntity $task): DateTimeInterface {
$date = new DateTimeImmutable('1900-01-01');
foreach ($task->getSubscribers() as $subscriber) {
$date = max($date, $subscriber->getUpdatedAt());
}
return $date;
}
}

View File

@ -2,19 +2,24 @@
namespace MailPoet\Test\Models;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterSegment;
use MailPoet\Models\ScheduledTask;
use MailPoet\Models\Segment;
use MailPoet\Models\SendingQueue;
use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Test\DataFactories\NewsletterOption as NewsletterOptionFactory;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
use MailPoet\Test\DataFactories\SendingQueue as SendingQueueFactory;
use MailPoet\Util\Security;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class NewsletterTest extends \MailPoetTest {
/** @var SendingQueueEntity */
public $sendingQueue;
public $segment2;
public $segment1;
@ -24,8 +29,15 @@ class NewsletterTest extends \MailPoetTest {
/** @var NewsletterOptionFactory */
private $newsletterOptionFactory;
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var NewsletterEntity */
private $newsletterEntity;
public function _before() {
parent::_before();
$this->newslettersRepository = $this->diContainer->get(NewslettersRepository::class);
$this->newsletter = Newsletter::createOrUpdate([
'subject' => 'My Standard Newsletter',
'preheader' => 'Pre Header',
@ -48,10 +60,11 @@ class NewsletterTest extends \MailPoetTest {
$association->segmentId = $this->segment2->id;
$association->save();
$this->sendingQueue = SendingTask::create();
$this->sendingQueue->newsletter_id = $this->newsletter->id;
$this->sendingQueue->status = ScheduledTask::STATUS_SCHEDULED;
$this->sendingQueue->save();
$newsletterEntity = $this->newslettersRepository->findOneById($this->newsletter->id);
$this->assertInstanceOf(NewsletterEntity::class, $newsletterEntity);
$this->newsletterEntity = $newsletterEntity;
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueueWorker::TASK_TYPE, ScheduledTask::STATUS_SCHEDULED);
$this->sendingQueue = (new SendingQueueFactory())->create($scheduledTask, $this->newsletterEntity);
$this->newsletterOptionFactory = new NewsletterOptionFactory();
}
@ -116,12 +129,6 @@ class NewsletterTest extends \MailPoetTest {
verify($isTimeUpdated)->true();
}
public function testItCanBeQueued() {
$queue = $this->newsletter->getQueue();
verify($queue->id > 0)->true();
verify($queue->newsletterId)->equals($this->newsletter->id);
}
public function testItCanHaveSegments() {
$newsletterSegments = $this->newsletter->segments()->findArray();
verify($newsletterSegments)->arrayCount(2);
@ -203,7 +210,7 @@ class NewsletterTest extends \MailPoetTest {
}
public function testItGetsQueueFromNewsletter() {
verify($this->newsletter->queue()->findOne()->id)->equals($this->sendingQueue->id);
verify($this->newsletter->queue()->findOne()->id)->equals($this->sendingQueue->getId());
}
public function testItCanBeRestored() {
@ -248,9 +255,8 @@ class NewsletterTest extends \MailPoetTest {
// create multiple sending queues
for ($i = 1; $i <= 5; $i++) {
$sendingQueue = SendingTask::create();
$sendingQueue->newsletterId = $newsletter->id;
$sendingQueue->save();
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueueWorker::TASK_TYPE, null);
(new SendingQueueFactory())->create($scheduledTask, $this->newsletterEntity);
}
// make sure relations exist
@ -276,9 +282,9 @@ class NewsletterTest extends \MailPoetTest {
'parent_id' => $parentNewsletter->id,
]
);
$sendingQueue = SendingTask::create();
$sendingQueue->newsletterId = $newsletter->id;
$sendingQueue->save();
$newsletterEntity = $this->newslettersRepository->findOneById($newsletter->id);
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueueWorker::TASK_TYPE, null);
(new SendingQueueFactory())->create($scheduledTask, $newsletterEntity);
$newsletterSegment = NewsletterSegment::create();
$newsletterSegment->newsletterId = $newsletter->id;
$newsletterSegment->segmentId = 1;
@ -302,9 +308,8 @@ class NewsletterTest extends \MailPoetTest {
// create multiple sending queues
$newsletter = $this->newsletter;
for ($i = 1; $i <= 5; $i++) {
$sendingQueue = SendingTask::create();
$sendingQueue->newsletterId = $newsletter->id;
$sendingQueue->save();
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueueWorker::TASK_TYPE, null);
(new SendingQueueFactory())->create($scheduledTask, $this->newsletterEntity);
}
verify(SendingQueue::whereNull('deleted_at')->findArray())->arrayCount(6);
@ -325,9 +330,8 @@ class NewsletterTest extends \MailPoetTest {
'parent_id' => $parentNewsletter->id,
]
);
$sendingQueue = SendingTask::create();
$sendingQueue->newsletterId = $newsletter->id;
$sendingQueue->save();
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueueWorker::TASK_TYPE, null);
(new SendingQueueFactory())->create($scheduledTask, $this->newsletterEntity);
}
// 1 parent and 5 children queues/newsletters
verify(Newsletter::whereNull('deleted_at')->findArray())->arrayCount(6);
@ -342,19 +346,19 @@ class NewsletterTest extends \MailPoetTest {
public function testItRestoresTrashedQueueAssociationsWhenNewsletterIsRestored() {
// create multiple sending queues
$sendingTasks = [];
$scheduledTasks = [];
$newsletter = $this->newsletter;
for ($i = 1; $i <= 5; $i++) {
$sendingTask = SendingTask::create();
$sendingTask->newsletterId = $newsletter->id;
$sendingTask->deletedAt = date('Y-m-d H:i:s');
$sendingTask->status = ScheduledTask::STATUS_SCHEDULED;
$sendingTask->save();
$sendingTasks[] = $sendingTask;
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueueWorker::TASK_TYPE, ScheduledTask::STATUS_SCHEDULED);
(new SendingQueueFactory())->create($scheduledTask, $this->newsletterEntity, new Carbon());
$scheduledTasks[] = $scheduledTask;
}
$inProgressTask = $sendingTasks[1];
$inProgressTask->status = null;
$inProgressTask->save();
$inProgressTask = $scheduledTasks[1];
$inProgressTask->setStatus(null);
$this->entityManager->persist($inProgressTask);
$this->entityManager->flush();
verify(SendingQueue::whereNotNull('deleted_at')->findArray())->arrayCount(5);
// restore newsletter and check that relations are restored
$newsletter->restore();
@ -369,9 +373,9 @@ class NewsletterTest extends \MailPoetTest {
$parentNewsletter = $this->newsletter;
$parentNewsletter->deletedAt = date('Y-m-d H:i:s');
$parentNewsletter->save();
$parentSendingQueue = $this->sendingQueue;
$parentSendingQueue->deletedAt = date('Y-m-d H:i:s');
$parentSendingQueue->save();
$this->sendingQueue->setDeletedAt(new Carbon());
$this->entityManager->persist($this->sendingQueue);
$this->entityManager->flush();
// create multiple children (post notification history) newsletters and sending queues
for ($i = 1; $i <= 5; $i++) {
@ -383,10 +387,8 @@ class NewsletterTest extends \MailPoetTest {
'deleted_at' => date('Y-m-d H:i:s'),
]
);
$sendingQueue = SendingTask::create();
$sendingQueue->newsletterId = $newsletter->id;
$sendingQueue->deletedAt = date('Y-m-d H:i:s');
$sendingQueue->save();
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueueWorker::TASK_TYPE, null);
(new SendingQueueFactory())->create($scheduledTask, $this->newsletterEntity, new Carbon());
}
// 1 parent and 5 children queues/newsletters
verify(Newsletter::whereNotNull('deleted_at')->findArray())->arrayCount(6);

View File

@ -0,0 +1,317 @@
<?php declare(strict_types = 1);
namespace MailPoet\Test\Newsletter;
use Codeception\Util\Fixtures;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterLinkEntity;
use MailPoet\Entities\NewsletterOptionEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\NewsletterPostEntity;
use MailPoet\Entities\NewsletterSegmentEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\Entities\StatsNotificationEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\WpPostEntity;
use MailPoet\Newsletter\NewsletterDeleteController;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Test\DataFactories\NewsletterOptionField;
use MailPoet\WP\Functions as WPFunctions;
class NewsletterDeleteControllerTest extends \MailPoetTest {
private NewsletterDeleteController $controller;
private NewslettersRepository $repository;
private ScheduledTaskSubscribersRepository $taskSubscribersRepository;
private WPFunctions $wp;
public function _before() {
parent::_before();
$this->controller = $this->diContainer->get(NewsletterDeleteController::class);
$this->repository = $this->diContainer->get(NewslettersRepository::class);
$this->taskSubscribersRepository = $this->diContainer->get(ScheduledTaskSubscribersRepository::class);
$this->wp = $this->diContainer->get(WPFunctions::class);
}
public function testItBulkDeleteNewslettersAndChildren() {
$standardNewsletter = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENDING);
$standardQueue = $this->createQueueWithTaskAndSegmentAndSubscribers($standardNewsletter, null); // Null for scheduled task being processed
$notification = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION, NewsletterEntity::STATUS_ACTIVE);
$notificationHistory = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SCHEDULED, $notification);
$notificationHistoryQueue = $this->createQueueWithTaskAndSegmentAndSubscribers($notificationHistory);
$standardSegment = $standardNewsletter->getNewsletterSegments()->first();
$this->assertInstanceOf(NewsletterSegmentEntity::class, $standardSegment);
$standardScheduledTaks = $standardQueue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $standardScheduledTaks);
$standardScheduledTaskSubscriber = $this->taskSubscribersRepository->findOneBy(['task' => $standardScheduledTaks]);
$this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $standardScheduledTaskSubscriber);
$notificationHistoryScheduledTask = $notificationHistoryQueue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $notificationHistoryScheduledTask);
$notificationHistorySegment = $notificationHistory->getNewsletterSegments()->first();
$this->assertInstanceOf(NewsletterSegmentEntity::class, $notificationHistorySegment);
$notificationHistoryScheduledTaskSubscriber = $this->taskSubscribersRepository->findOneBy(['task' => $notificationHistoryScheduledTask]);
$this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $notificationHistoryScheduledTaskSubscriber);
$standardStatsNotification = $this->createStatNotification($standardNewsletter);
$standardStatsNotificationScheduledTask = $standardStatsNotification->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $standardStatsNotificationScheduledTask);
$notificationHistoryStatsNotification = $this->createStatNotification($notificationHistory);
$notificationHistoryStatsNotificationScheduledTask = $notificationHistoryStatsNotification->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $notificationHistoryStatsNotificationScheduledTask);
$standardLink = $this->createNewsletterLink($standardNewsletter, $standardQueue);
$notificationHistoryLink = $this->createNewsletterLink($notificationHistory, $notificationHistoryQueue);
$optionField = (new NewsletterOptionField())->findOrCreate('name', NewsletterEntity::TYPE_NOTIFICATION);
$optionValue = $this->createNewsletterOption($notificationHistory, $optionField, 'value');
$newsletterPost = $this->createNewsletterPost($notification, 1);
$subscriber = $standardScheduledTaskSubscriber->getSubscriber();
$this->assertInstanceOf(SubscriberEntity::class, $subscriber);
$statisticsNewsletter = $this->createNewsletterStatistics($standardNewsletter, $standardQueue, $subscriber);
$statisticsOpen = $this->createOpenStatistics($standardNewsletter, $standardQueue, $subscriber);
$statisticsClick = $this->createClickStatistics($standardNewsletter, $standardQueue, $subscriber, $standardLink);
$statisticsPurchase = $this->createPurchaseStatistics($standardNewsletter, $standardQueue, $statisticsClick, $subscriber);
// Trash
$this->repository->bulkTrash([(int)$standardNewsletter->getId(), (int)$notification->getId()]);
// Delete
$this->controller->bulkDelete([(int)$standardNewsletter->getId(), (int)$notification->getId()]);
// Clear entity manager to forget all entities
$this->entityManager->clear();
// Check they were all deleted
// Newsletters
verify($this->repository->findOneById($standardNewsletter->getId()))->null();
verify($this->repository->findOneById($notification->getId()))->null();
verify($this->repository->findOneById($notificationHistory->getId()))->null();
// Sending queues
verify($this->entityManager->find(SendingQueueEntity::class, $standardQueue->getId()))->null();
verify($this->entityManager->find(SendingQueueEntity::class, $notificationHistoryQueue->getId()))->null();
// Scheduled tasks subscribers
verify($this->taskSubscribersRepository->findOneBy(['task' => $standardScheduledTaks]))->null();
verify($this->taskSubscribersRepository->findOneBy(['task' => $notificationHistoryScheduledTask]))->null();
// Scheduled tasks
verify($this->entityManager->find(ScheduledTaskEntity::class, $standardScheduledTaks->getId()))->null();
verify($this->entityManager->find(ScheduledTaskEntity::class, $notificationHistoryScheduledTask->getId()))->null();
// Newsletter segments
verify($this->entityManager->find(NewsletterSegmentEntity::class, $standardSegment->getId()))->null();
verify($this->entityManager->find(NewsletterSegmentEntity::class, $notificationHistorySegment->getId()))->null();
// Newsletter stats notifications
verify($this->entityManager->find(StatsNotificationEntity::class, $standardStatsNotificationScheduledTask->getId()))->null();
verify($this->entityManager->find(StatsNotificationEntity::class, $notificationHistoryStatsNotification->getId()))->null();
// Newsletter stats notifications scheduled tasks
verify($this->entityManager->find(ScheduledTaskEntity::class, $standardStatsNotificationScheduledTask->getId()))->null();
verify($this->entityManager->find(ScheduledTaskEntity::class, $notificationHistoryStatsNotificationScheduledTask->getId()))->null();
// Newsletter links
verify($this->entityManager->find(NewsletterLinkEntity::class, $standardLink->getId()))->null();
verify($this->entityManager->find(NewsletterLinkEntity::class, $notificationHistoryLink->getId()))->null();
// Option fields values
verify($this->entityManager->find(NewsletterOptionEntity::class, $optionValue->getId()))->null();
// Newsletter post
verify($this->entityManager->find(NewsletterPostEntity::class, $newsletterPost->getId()))->null();
// Statistics data
verify($this->entityManager->find(StatisticsNewsletterEntity::class, $statisticsNewsletter->getId()))->null();
verify($this->entityManager->find(StatisticsOpenEntity::class, $statisticsOpen->getId()))->null();
verify($this->entityManager->find(StatisticsClickEntity::class, $statisticsClick->getId()))->null();
$statisticsPurchase = $this->entityManager->find(StatisticsWooCommercePurchaseEntity::class, $statisticsPurchase->getId());
$this->assertNotNull($statisticsPurchase);
verify($statisticsPurchase->getNewsletter())->null();
}
public function testItDeletesMultipleNewslettersWithPurchaseStatsAndKeepsStats() {
$standardNewsletter1 = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENT);
$statisticsPurchase1 = $this->createPurchaseStatsForNewsletter($standardNewsletter1);
$standardNewsletter2 = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENT);
$statisticsPurchase2 = $this->createPurchaseStatsForNewsletter($standardNewsletter2);
// Delete
$this->controller->bulkDelete([(int)$standardNewsletter1->getId(), (int)$standardNewsletter2->getId()]);
// Clear entity manager to forget all entities
$this->entityManager->clear();
// Check Newsletters were deleted
verify($this->repository->findOneById($standardNewsletter1->getId()))->null();
verify($this->repository->findOneById($standardNewsletter2->getId()))->null();
// Check purchase stats were not deleted
$statisticsPurchase1 = $this->entityManager->find(StatisticsWooCommercePurchaseEntity::class, $statisticsPurchase1->getId());
$statisticsPurchase2 = $this->entityManager->find(StatisticsWooCommercePurchaseEntity::class, $statisticsPurchase2->getId());
$this->assertNotNull($statisticsPurchase1);
verify($statisticsPurchase1->getNewsletter())->null();
$this->assertNotNull($statisticsPurchase2);
verify($statisticsPurchase2->getNewsletter())->null();
}
public function testItDeletesWpPostsBulkDelete() {
$newsletter1 = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENDING);
$post1Id = $this->wp->wpInsertPost(['post_title' => 'Post 1']);
$newsletter1->setWpPost($this->entityManager->getReference(WpPostEntity::class, $post1Id));
$newsletter2 = $this->createNewsletter(NewsletterEntity::TYPE_WELCOME, NewsletterEntity::STATUS_SENDING);
$post2Id = $this->wp->wpInsertPost(['post_title' => 'Post 2']);
$newsletter2->setWpPost($this->entityManager->getReference(WpPostEntity::class, $post2Id));
$newsletter3 = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENDING);
$blogPost = $this->wp->wpInsertPost(['post_title' => 'Regular blog post']);
verify($this->wp->getPost($post1Id))->instanceOf(\WP_Post::class);
verify($this->wp->getPost($post2Id))->instanceOf(\WP_Post::class);
$this->entityManager->flush();
$this->entityManager->clear();
$this->controller->bulkDelete([(int)$newsletter1->getId(), (int)$newsletter2->getId(), (int)$newsletter3->getId()]);
verify($this->wp->getPost($post1Id))->null();
verify($this->wp->getPost($post2Id))->null();
verify($this->wp->getPost($blogPost))->instanceOf(\WP_Post::class);
}
private function createNewsletter(string $type, string $status = NewsletterEntity::STATUS_DRAFT, $parent = null): NewsletterEntity {
$newsletter = new NewsletterEntity();
$newsletter->setType($type);
$newsletter->setSubject('My Standard Newsletter');
$newsletter->setBody(Fixtures::get('newsletter_body_template'));
$newsletter->setStatus($status);
$newsletter->setParent($parent);
$this->entityManager->persist($newsletter);
$this->entityManager->flush();
return $newsletter;
}
private function createQueueWithTaskAndSegmentAndSubscribers(NewsletterEntity $newsletter, $status = ScheduledTaskEntity::STATUS_SCHEDULED): SendingQueueEntity {
$task = new ScheduledTaskEntity();
$task->setType(SendingQueue::TASK_TYPE);
$task->setStatus($status);
$this->entityManager->persist($task);
$queue = new SendingQueueEntity();
$queue->setNewsletter($newsletter);
$queue->setTask($task);
$this->entityManager->persist($queue);
$newsletter->getQueues()->add($queue);
$segment = new SegmentEntity("List for newsletter id {$newsletter->getId()}", SegmentEntity::TYPE_DEFAULT, 'Description');
$this->entityManager->persist($segment);
$subscriber = new SubscriberEntity();
$subscriber->setEmail("sub{$newsletter->getId()}@mailpoet.com");
$subscriber->setStatus(SubscriberEntity::STATUS_SUBSCRIBED);
$this->entityManager->persist($subscriber);
$this->entityManager->flush();
$scheduledTaskSubscriber = new ScheduledTaskSubscriberEntity($task, $subscriber);
$this->entityManager->persist($scheduledTaskSubscriber);
$newsletterSegment = new NewsletterSegmentEntity($newsletter, $segment);
$newsletter->getNewsletterSegments()->add($newsletterSegment);
$this->entityManager->persist($newsletterSegment);
$this->entityManager->flush();
return $queue;
}
private function createStatNotification(NewsletterEntity $newsletter): StatsNotificationEntity {
$task = new ScheduledTaskEntity();
$task->setType('stats_notification');
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
$this->entityManager->persist($task);
$statsNotification = new StatsNotificationEntity($newsletter, $task);
$this->entityManager->persist($statsNotification);
$this->entityManager->flush();
return $statsNotification;
}
private function createNewsletterLink(NewsletterEntity $newsletter, SendingQueueEntity $queue): NewsletterLinkEntity {
$link = new NewsletterLinkEntity($newsletter, $queue, 'http://example.com', 'abcd');
$this->entityManager->persist($link);
$this->entityManager->flush();
return $link;
}
private function createNewsletterOption(NewsletterEntity $newsletter, NewsletterOptionFieldEntity $field, $value): NewsletterOptionEntity {
$option = new NewsletterOptionEntity($newsletter, $field);
$option->setValue($value);
$this->entityManager->persist($option);
$this->entityManager->flush();
return $option;
}
private function createNewsletterPost(NewsletterEntity $newsletter, int $postId): NewsletterPostEntity {
$post = new NewsletterPostEntity($newsletter, $postId);
$this->entityManager->persist($post);
$this->entityManager->flush();
return $post;
}
private function createNewsletterStatistics(NewsletterEntity $newsletter, SendingQueueEntity $queue, SubscriberEntity $subscriber): StatisticsNewsletterEntity {
$statisticsNewsletter = new StatisticsNewsletterEntity($newsletter, $queue, $subscriber);
$this->entityManager->persist($statisticsNewsletter);
$this->entityManager->flush();
return $statisticsNewsletter;
}
private function createOpenStatistics(NewsletterEntity $newsletter, SendingQueueEntity $queue, SubscriberEntity $subscriber): StatisticsOpenEntity {
$statistics = new StatisticsOpenEntity($newsletter, $queue, $subscriber);
$this->entityManager->persist($statistics);
$this->entityManager->flush();
return $statistics;
}
private function createClickStatistics(
NewsletterEntity $newsletter,
SendingQueueEntity $queue,
SubscriberEntity $subscriber,
NewsletterLinkEntity $link
): StatisticsClickEntity {
$statistics = new StatisticsClickEntity($newsletter, $queue, $subscriber, $link, 1);
$this->entityManager->persist($statistics);
$this->entityManager->flush();
return $statistics;
}
private function createPurchaseStatistics(
NewsletterEntity $newsletter,
SendingQueueEntity $queue,
StatisticsClickEntity $click,
SubscriberEntity $subscriber
): StatisticsWooCommercePurchaseEntity {
$statistics = new StatisticsWooCommercePurchaseEntity($newsletter, $queue, $click, 1, 'EUR', 100, 'completed');
$statistics->setSubscriber($subscriber);
$this->entityManager->persist($statistics);
$this->entityManager->flush();
return $statistics;
}
private function createPurchaseStatsForNewsletter(NewsletterEntity $newsletter): StatisticsWooCommercePurchaseEntity {
$queue = $this->createQueueWithTaskAndSegmentAndSubscribers($newsletter, NewsletterEntity::STATUS_SENT); // Null for scheduled task being processed
$segment = $newsletter->getNewsletterSegments()->first();
$this->assertInstanceOf(NewsletterSegmentEntity::class, $segment);
$scheduledTask = $queue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $scheduledTask);
$scheduledTaskSubscriber = $this->taskSubscribersRepository->findOneBy(['task' => $scheduledTask]);
$this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $scheduledTaskSubscriber);
$link = $this->createNewsletterLink($newsletter, $queue);
$this->assertInstanceOf(NewsletterLinkEntity::class, $link);
$subscriber = $scheduledTaskSubscriber->getSubscriber();
$this->assertInstanceOf(SubscriberEntity::class, $subscriber);
$statisticsClick = $this->createClickStatistics($newsletter, $queue, $subscriber, $link);
$this->assertInstanceOf(StatisticsClickEntity::class, $statisticsClick);
return $this->createPurchaseStatistics($newsletter, $queue, $statisticsClick, $subscriber);
}
}

View File

@ -5,43 +5,23 @@ namespace MailPoet\Newsletter;
use Codeception\Util\Fixtures;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterLinkEntity;
use MailPoet\Entities\NewsletterOptionEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\NewsletterPostEntity;
use MailPoet\Entities\NewsletterSegmentEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\Entities\StatsNotificationEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\WpPostEntity;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\Test\DataFactories\NewsletterOptionField;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
use MailPoet\Test\DataFactories\SendingQueue as SendingQueueFactory;
use MailPoetVendor\Carbon\Carbon;
class NewsletterRepositoryTest extends \MailPoetTest {
/** @var NewslettersRepository */
private $repository;
/** @var ScheduledTaskSubscribersRepository */
private $taskSubscribersRepository;
/** @var WPFunctions */
private $wp;
public function _before() {
parent::_before();
$this->repository = $this->diContainer->get(NewslettersRepository::class);
$this->taskSubscribersRepository = $this->diContainer->get(ScheduledTaskSubscribersRepository::class);
$this->wp = $this->diContainer->get(WPFunctions::class);
}
public function testItBulkTrashNewslettersAndChildren() {
@ -126,149 +106,6 @@ class NewsletterRepositoryTest extends \MailPoetTest {
verify($scheduledTask->getStatus())->equals(ScheduledTaskEntity::STATUS_SCHEDULED);
}
public function testItBulkDeleteNewslettersAndChildren() {
$standardNewsletter = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENDING);
$standardQueue = $this->createQueueWithTaskAndSegmentAndSubscribers($standardNewsletter, null); // Null for scheduled task being processed
$notification = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION, NewsletterEntity::STATUS_ACTIVE);
$notificationHistory = $this->createNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, NewsletterEntity::STATUS_SCHEDULED, $notification);
$notificationHistoryQueue = $this->createQueueWithTaskAndSegmentAndSubscribers($notificationHistory);
$standardSegment = $standardNewsletter->getNewsletterSegments()->first();
$this->assertInstanceOf(NewsletterSegmentEntity::class, $standardSegment);
$standardScheduledTaks = $standardQueue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $standardScheduledTaks);
$standardScheduledTaskSubscriber = $this->taskSubscribersRepository->findOneBy(['task' => $standardScheduledTaks]);
$this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $standardScheduledTaskSubscriber);
$notificationHistoryScheduledTask = $notificationHistoryQueue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $notificationHistoryScheduledTask);
$notificationHistorySegment = $notificationHistory->getNewsletterSegments()->first();
$this->assertInstanceOf(NewsletterSegmentEntity::class, $notificationHistorySegment);
$notificationHistoryScheduledTaskSubscriber = $this->taskSubscribersRepository->findOneBy(['task' => $notificationHistoryScheduledTask]);
$this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $notificationHistoryScheduledTaskSubscriber);
$standardStatsNotification = $this->createStatNotification($standardNewsletter);
$standardStatsNotificationScheduledTask = $standardStatsNotification->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $standardStatsNotificationScheduledTask);
$notificationHistoryStatsNotification = $this->createStatNotification($notificationHistory);
$notificationHistoryStatsNotificationScheduledTask = $notificationHistoryStatsNotification->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $notificationHistoryStatsNotificationScheduledTask);
$standardLink = $this->createNewsletterLink($standardNewsletter, $standardQueue);
$notificationHistoryLink = $this->createNewsletterLink($notificationHistory, $notificationHistoryQueue);
$optionField = (new NewsletterOptionField())->findOrCreate('name', NewsletterEntity::TYPE_NOTIFICATION);
$optionValue = $this->createNewsletterOption($notificationHistory, $optionField, 'value');
$newsletterPost = $this->createNewsletterPost($notification, 1);
$subscriber = $standardScheduledTaskSubscriber->getSubscriber();
$this->assertInstanceOf(SubscriberEntity::class, $subscriber);
$statisticsNewsletter = $this->createNewsletterStatistics($standardNewsletter, $standardQueue, $subscriber);
$statisticsOpen = $this->createOpenStatistics($standardNewsletter, $standardQueue, $subscriber);
$statisticsClick = $this->createClickStatistics($standardNewsletter, $standardQueue, $subscriber, $standardLink);
$statisticsPurchase = $this->createPurchaseStatistics($standardNewsletter, $standardQueue, $statisticsClick, $subscriber);
// Trash
$this->repository->bulkTrash([$standardNewsletter->getId(), $notification->getId()]);
// Delete
$this->repository->bulkDelete([$standardNewsletter->getId(), $notification->getId()]);
// Clear entity manager to forget all entities
$this->entityManager->clear();
// Check they were all deleted
// Newsletters
verify($this->repository->findOneById($standardNewsletter->getId()))->null();
verify($this->repository->findOneById($notification->getId()))->null();
verify($this->repository->findOneById($notificationHistory->getId()))->null();
// Sending queues
verify($this->entityManager->find(SendingQueueEntity::class, $standardQueue->getId()))->null();
verify($this->entityManager->find(SendingQueueEntity::class, $notificationHistoryQueue->getId()))->null();
// Scheduled tasks subscribers
verify($this->taskSubscribersRepository->findOneBy(['task' => $standardScheduledTaks]))->null();
verify($this->taskSubscribersRepository->findOneBy(['task' => $notificationHistoryScheduledTask]))->null();
// Scheduled tasks
verify($this->entityManager->find(ScheduledTaskEntity::class, $standardScheduledTaks->getId()))->null();
verify($this->entityManager->find(ScheduledTaskEntity::class, $notificationHistoryScheduledTask->getId()))->null();
// Newsletter segments
verify($this->entityManager->find(NewsletterSegmentEntity::class, $standardSegment->getId()))->null();
verify($this->entityManager->find(NewsletterSegmentEntity::class, $notificationHistorySegment->getId()))->null();
// Newsletter stats notifications
verify($this->entityManager->find(StatsNotificationEntity::class, $standardStatsNotificationScheduledTask->getId()))->null();
verify($this->entityManager->find(StatsNotificationEntity::class, $notificationHistoryStatsNotification->getId()))->null();
// Newsletter stats notifications scheduled tasks
verify($this->entityManager->find(ScheduledTaskEntity::class, $standardStatsNotificationScheduledTask->getId()))->null();
verify($this->entityManager->find(ScheduledTaskEntity::class, $notificationHistoryStatsNotificationScheduledTask->getId()))->null();
// Newsletter links
verify($this->entityManager->find(NewsletterLinkEntity::class, $standardLink->getId()))->null();
verify($this->entityManager->find(NewsletterLinkEntity::class, $notificationHistoryLink->getId()))->null();
// Option fields values
verify($this->entityManager->find(NewsletterOptionEntity::class, $optionValue->getId()))->null();
// Newsletter post
verify($this->entityManager->find(NewsletterPostEntity::class, $newsletterPost->getId()))->null();
// Statistics data
verify($this->entityManager->find(StatisticsNewsletterEntity::class, $statisticsNewsletter->getId()))->null();
verify($this->entityManager->find(StatisticsOpenEntity::class, $statisticsOpen->getId()))->null();
verify($this->entityManager->find(StatisticsClickEntity::class, $statisticsClick->getId()))->null();
$statisticsPurchase = $this->entityManager->find(StatisticsWooCommercePurchaseEntity::class, $statisticsPurchase->getId());
$this->assertNotNull($statisticsPurchase);
verify($statisticsPurchase->getNewsletter())->null();
}
public function testItDeletesMultipleNewslettersWithPurchaseStatsAndKeepsStats() {
$standardNewsletter1 = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENT);
$statisticsPurchase1 = $this->createPurchaseStatsForNewsletter($standardNewsletter1);
$standardNewsletter2 = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENT);
$statisticsPurchase2 = $this->createPurchaseStatsForNewsletter($standardNewsletter2);
// Delete
$this->repository->bulkDelete([$standardNewsletter1->getId(), $standardNewsletter2->getId()]);
// Clear entity manager to forget all entities
$this->entityManager->clear();
// Check Newsletters were deleted
verify($this->repository->findOneById($standardNewsletter1->getId()))->null();
verify($this->repository->findOneById($standardNewsletter2->getId()))->null();
// Check purchase stats were not deleted
$statisticsPurchase1 = $this->entityManager->find(StatisticsWooCommercePurchaseEntity::class, $statisticsPurchase1->getId());
$statisticsPurchase2 = $this->entityManager->find(StatisticsWooCommercePurchaseEntity::class, $statisticsPurchase2->getId());
$this->assertNotNull($statisticsPurchase1);
verify($statisticsPurchase1->getNewsletter())->null();
$this->assertNotNull($statisticsPurchase2);
verify($statisticsPurchase2->getNewsletter())->null();
}
public function testItDeletesWpPostsBulkDelete() {
$newsletter1 = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENDING);
$post1Id = $this->wp->wpInsertPost(['post_title' => 'Post 1']);
$newsletter1->setWpPost($this->entityManager->getReference(WpPostEntity::class, $post1Id));
$newsletter2 = $this->createNewsletter(NewsletterEntity::TYPE_WELCOME, NewsletterEntity::STATUS_SENDING);
$post2Id = $this->wp->wpInsertPost(['post_title' => 'Post 2']);
$newsletter2->setWpPost($this->entityManager->getReference(WpPostEntity::class, $post2Id));
$newsletter3 = $this->createNewsletter(NewsletterEntity::TYPE_STANDARD, NewsletterEntity::STATUS_SENDING);
$blogPost = $this->wp->wpInsertPost(['post_title' => 'Regular blog post']);
verify($this->wp->getPost($post1Id))->instanceOf(\WP_Post::class);
verify($this->wp->getPost($post2Id))->instanceOf(\WP_Post::class);
$this->entityManager->flush();
$this->entityManager->clear();
$this->repository->bulkDelete([$newsletter1->getId(), $newsletter2->getId(), $newsletter3->getId()]);
verify($this->wp->getPost($post1Id))->null();
verify($this->wp->getPost($post2Id))->null();
verify($this->wp->getPost($blogPost))->instanceOf(\WP_Post::class);
}
public function testItGetsArchiveNewslettersForSegments() {
$types = [
NewsletterEntity::TYPE_STANDARD,
@ -306,11 +143,11 @@ class NewsletterRepositoryTest extends \MailPoetTest {
NewsletterEntity::TYPE_NOTIFICATION_HISTORY,
];
list($newsletters, $sendingQueues) = $this->createNewslettersAndSendingTasks($types);
list($newsletters, $scheduledTasks) = $this->createNewslettersAndSendingTasks($types);
// set the sending queue status of the first newsletter to null
$sendingQueues[0]->status = null;
$sendingQueues[0]->save();
$scheduledTasks[0]->setStatus(null);
$this->entityManager->persist($scheduledTasks[0]);
// trash the last newsletter
end($newsletters)->setDeletedAt(new Carbon());
@ -345,17 +182,15 @@ class NewsletterRepositoryTest extends \MailPoetTest {
private function createNewslettersAndSendingTasks(array $types): array {
$newsletters = [];
$sendingQueues = [];
$scheduledTasks = [];
for ($i = 0; $i < count($types); $i++) {
$newsletters[$i] = $this->createNewsletter($types[$i]);
$sendingQueues[$i] = SendingTask::create();
$sendingQueues[$i]->newsletter_id = $newsletters[$i]->getId();
$sendingQueues[$i]->status = SendingQueueEntity::STATUS_COMPLETED;
$sendingQueues[$i]->save();
$scheduledTasks[$i] = (new ScheduledTaskFactory())->create(SendingQueue::TASK_TYPE, SendingQueueEntity::STATUS_COMPLETED);
(new SendingQueueFactory())->create($scheduledTasks[$i], $newsletters[$i]);
}
return [$newsletters, $sendingQueues];
return [$newsletters, $scheduledTasks];
}
private function createQueueWithTaskAndSegmentAndSubscribers(NewsletterEntity $newsletter, $status = ScheduledTaskEntity::STATUS_SCHEDULED): SendingQueueEntity {
@ -387,93 +222,4 @@ class NewsletterRepositoryTest extends \MailPoetTest {
$this->entityManager->flush();
return $queue;
}
private function createStatNotification(NewsletterEntity $newsletter): StatsNotificationEntity {
$task = new ScheduledTaskEntity();
$task->setType('stats_notification');
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
$this->entityManager->persist($task);
$statsNotification = new StatsNotificationEntity($newsletter, $task);
$this->entityManager->persist($statsNotification);
$this->entityManager->flush();
return $statsNotification;
}
private function createNewsletterLink(NewsletterEntity $newsletter, SendingQueueEntity $queue): NewsletterLinkEntity {
$link = new NewsletterLinkEntity($newsletter, $queue, 'http://example.com', 'abcd');
$this->entityManager->persist($link);
$this->entityManager->flush();
return $link;
}
private function createNewsletterOption(NewsletterEntity $newsletter, NewsletterOptionFieldEntity $field, $value): NewsletterOptionEntity {
$option = new NewsletterOptionEntity($newsletter, $field);
$option->setValue($value);
$this->entityManager->persist($option);
$this->entityManager->flush();
return $option;
}
private function createNewsletterPost(NewsletterEntity $newsletter, int $postId): NewsletterPostEntity {
$post = new NewsletterPostEntity($newsletter, $postId);
$this->entityManager->persist($post);
$this->entityManager->flush();
return $post;
}
private function createNewsletterStatistics(NewsletterEntity $newsletter, SendingQueueEntity $queue, SubscriberEntity $subscriber): StatisticsNewsletterEntity {
$statisticsNewsletter = new StatisticsNewsletterEntity($newsletter, $queue, $subscriber);
$this->entityManager->persist($statisticsNewsletter);
$this->entityManager->flush();
return $statisticsNewsletter;
}
private function createOpenStatistics(NewsletterEntity $newsletter, SendingQueueEntity $queue, SubscriberEntity $subscriber): StatisticsOpenEntity {
$statistics = new StatisticsOpenEntity($newsletter, $queue, $subscriber);
$this->entityManager->persist($statistics);
$this->entityManager->flush();
return $statistics;
}
private function createClickStatistics(
NewsletterEntity $newsletter,
SendingQueueEntity $queue,
SubscriberEntity $subscriber,
NewsletterLinkEntity $link
): StatisticsClickEntity {
$statistics = new StatisticsClickEntity($newsletter, $queue, $subscriber, $link, 1);
$this->entityManager->persist($statistics);
$this->entityManager->flush();
return $statistics;
}
private function createPurchaseStatistics(
NewsletterEntity $newsletter,
SendingQueueEntity $queue,
StatisticsClickEntity $click,
SubscriberEntity $subscriber
): StatisticsWooCommercePurchaseEntity {
$statistics = new StatisticsWooCommercePurchaseEntity($newsletter, $queue, $click, 1, 'EUR', 100, 'completed');
$statistics->setSubscriber($subscriber);
$this->entityManager->persist($statistics);
$this->entityManager->flush();
return $statistics;
}
private function createPurchaseStatsForNewsletter(NewsletterEntity $newsletter): StatisticsWooCommercePurchaseEntity {
$queue = $this->createQueueWithTaskAndSegmentAndSubscribers($newsletter, NewsletterEntity::STATUS_SENT); // Null for scheduled task being processed
$segment = $newsletter->getNewsletterSegments()->first();
$this->assertInstanceOf(NewsletterSegmentEntity::class, $segment);
$scheduledTask = $queue->getTask();
$this->assertInstanceOf(ScheduledTaskEntity::class, $scheduledTask);
$scheduledTaskSubscriber = $this->taskSubscribersRepository->findOneBy(['task' => $scheduledTask]);
$this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $scheduledTaskSubscriber);
$link = $this->createNewsletterLink($newsletter, $queue);
$this->assertInstanceOf(NewsletterLinkEntity::class, $link);
$subscriber = $scheduledTaskSubscriber->getSubscriber();
$this->assertInstanceOf(SubscriberEntity::class, $subscriber);
$statisticsClick = $this->createClickStatistics($newsletter, $queue, $subscriber, $link);
$this->assertInstanceOf(StatisticsClickEntity::class, $statisticsClick);
return $this->createPurchaseStatistics($newsletter, $queue, $statisticsClick, $subscriber);
}
}

View File

@ -2,6 +2,7 @@
namespace MailPoet\Newsletter\Scheduler;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SegmentEntity;
@ -9,11 +10,13 @@ use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\Test\DataFactories\NewsletterOption;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
use MailPoet\Test\DataFactories\SendingQueue as SendingQueueFactory;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager;
@ -41,6 +44,9 @@ class WelcomeTest extends \MailPoetTest {
/** @var NewsletterEntity */
private $newsletter;
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
public function _before() {
parent::_before();
$this->segmentRepository = $this->diContainer->get(SegmentsRepository::class);
@ -52,6 +58,7 @@ class WelcomeTest extends \MailPoetTest {
$this->wpSegment->setType(SegmentEntity::TYPE_WP_USERS);
$this->segmentRepository->flush();
$this->newsletter = $this->createWelcomeNewsletter();
$this->scheduledTaskSubscribersRepository = $this->diContainer->get(ScheduledTaskSubscribersRepository::class);
}
public function testItDoesNotCreateDuplicateWelcomeNotificationSendingTasks() {
@ -63,10 +70,10 @@ class WelcomeTest extends \MailPoetTest {
]);
$existingSubscriber = $this->subscriber->getId();
$existingQueue = SendingTask::create();
$existingQueue->newsletterId = $newsletter->getId();
$existingQueue->setSubscribers([$existingSubscriber]);
$existingQueue->save();
$scheduledTask = (new ScheduledTaskFactory())->create(SendingQueue::TASK_TYPE, null);
(new SendingQueueFactory())->create($scheduledTask, $newsletter);
$this->scheduledTaskSubscribersRepository->setSubscribers($scheduledTask, [$existingSubscriber]);
// queue is not scheduled
$this->welcomeScheduler->createWelcomeNotificationSendingTask($newsletter, $existingSubscriber);

View File

@ -3,16 +3,20 @@
namespace MailPoet\Newsletter\ViewInBrowser;
use Codeception\Stub\Expected;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Newsletter\Url;
use MailPoet\Subscribers\LinkTokens;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\Test\DataFactories\Newsletter;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
use MailPoet\Test\DataFactories\SendingQueue as SendingQueueFactory;
use MailPoet\Util\Security;
class ViewInBrowserControllerTest extends \MailPoetTest {
@ -28,9 +32,6 @@ class ViewInBrowserControllerTest extends \MailPoetTest {
/** @var SubscriberEntity */
private $subscriber;
/** @var SendingTask */
private $sendingTask;
/** @var mixed[] */
private $browserPreviewData;
@ -43,6 +44,15 @@ class ViewInBrowserControllerTest extends \MailPoetTest {
/** @var Url */
private $newsletterUrl;
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
/** @var ScheduledTaskEntity */
private $scheduledTask;
/** @var SendingQueueEntity */
private $sendingQueue;
public function _before() {
// instantiate class
$this->viewInBrowserController = $this->diContainer->get(ViewInBrowserController::class);
@ -50,6 +60,7 @@ class ViewInBrowserControllerTest extends \MailPoetTest {
$this->subscribersRepository = $this->diContainer->get(SubscribersRepository::class);
$this->sendingQueuesRepository = $this->diContainer->get(SendingQueuesRepository::class);
$this->newslettersRepository = $this->diContainer->get(NewslettersRepository::class);
$this->scheduledTaskSubscribersRepository = $this->diContainer->get(ScheduledTaskSubscribersRepository::class);
$this->newsletterUrl = $this->diContainer->get(Url::class);
// create newsletter
@ -66,16 +77,14 @@ class ViewInBrowserControllerTest extends \MailPoetTest {
$this->subscribersRepository->flush();
$this->subscriber = $subscriber;
// create task & queue
$sendingTask = SendingTask::create();
$sendingTask->newsletterId = $newsletter->getId();
$sendingTask->setSubscribers([$subscriber->getId()]);
$sendingTask->updateProcessedSubscribers([$subscriber->getId()]);
$this->sendingTask = $sendingTask->save();
$this->scheduledTask = (new ScheduledTaskFactory())->create(SendingQueue::TASK_TYPE, null);
$this->sendingQueue = (new SendingQueueFactory())->create($this->scheduledTask, $newsletter);
$this->scheduledTaskSubscribersRepository->setSubscribers($this->scheduledTask, [$subscriber->getId()]);
$this->scheduledTaskSubscribersRepository->updateProcessedSubscribers($this->scheduledTask, [(int)$subscriber->getId()]);
// build browser preview data
$this->browserPreviewData = [
'queue_id' => $sendingTask->queue()->id,
'queue_id' => $this->sendingQueue->getId(),
'subscriber_id' => $subscriber->getId(),
'newsletter_id' => $newsletter->getId(),
'newsletter_hash' => $newsletter->getHash(),
@ -120,10 +129,8 @@ class ViewInBrowserControllerTest extends \MailPoetTest {
public function testItThrowsWhenSubscriberIsNotOnProcessedList() {
$data = $this->browserPreviewData;
$sendingTask = $this->sendingTask;
$sendingTask->setSubscribers([]);
$sendingTask->updateProcessedSubscribers([]);
$sendingTask->save();
$this->scheduledTaskSubscribersRepository->setSubscribers($this->scheduledTask, []);
$this->scheduledTaskSubscribersRepository->updateProcessedSubscribers($this->scheduledTask, []);
$this->expectViewThrowsExceptionWithMessage($this->viewInBrowserController, $data, 'Subscriber did not receive the newsletter yet');
}
@ -171,14 +178,14 @@ class ViewInBrowserControllerTest extends \MailPoetTest {
'render' => Expected::once(function (bool $isPreview, NewsletterEntity $newsletter, SubscriberEntity $subscriber = null, SendingQueueEntity $queue = null) {
$this->assertNotNull($queue); // PHPStan
verify($queue)->notNull();
verify($queue->getId())->equals($this->sendingTask->id);
verify($queue->getId())->equals($this->sendingQueue->getId());
}),
]);
$viewInBrowserController = $this->createController($viewInBrowserRenderer);
$data = $this->browserPreviewData;
$data['queueId'] = $this->sendingTask->queue()->id;
$data['queueId'] = $this->sendingQueue->getId();
$viewInBrowserController->view($data);
}
@ -187,7 +194,7 @@ class ViewInBrowserControllerTest extends \MailPoetTest {
'render' => Expected::once(function (bool $isPreview, NewsletterEntity $newsletter, SubscriberEntity $subscriber = null, SendingQueueEntity $queue = null) {
$this->assertNotNull($queue); // PHPStan
verify($queue)->notNull();
verify($queue->getId())->equals($this->sendingTask->queue()->id);
verify($queue->getId())->equals($this->sendingQueue->getId());
}),
]);

View File

@ -3,6 +3,7 @@
namespace MailPoet\Newsletter\ViewInBrowser;
use Codeception\Stub\Expected;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
@ -10,16 +11,16 @@ use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\Links\Links;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoet\Router\Router;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\Test\DataFactories\Newsletter;
use MailPoet\Test\DataFactories\NewsletterLink;
use MailPoet\Test\DataFactories\ScheduledTask as ScheduledTaskFactory;
use MailPoet\Test\DataFactories\SendingQueue as SendingQueueFactory;
use MailPoet\WP\Emoji;
class ViewInBrowserRendererTest extends \MailPoetTest {
@ -32,9 +33,6 @@ class ViewInBrowserRendererTest extends \MailPoetTest {
/** @var NewsletterEntity */
public $newsletter;
/** @var SendingTask */
private $sendingTask;
/** @var SubscriberEntity */
private $subscriber;
@ -47,16 +45,19 @@ class ViewInBrowserRendererTest extends \MailPoetTest {
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var SendingQueuesRepository */
private $sendingQueueRepository;
/** @var NewslettersRepository */
private $newsletterRepository;
/** @var ScheduledTaskEntity */
private $scheduledTask;
/** @var SendingQueueEntity */
private $sendingQueue;
public function _before() {
$this->sendingQueueRepository = $this->diContainer->get(SendingQueuesRepository::class);
$this->subscribersRepository = $this->diContainer->get(SubscribersRepository::class);
$this->newsletterRepository = $this->diContainer->get(NewslettersRepository::class);
$scheduledTaskSubscribersRepository = $this->diContainer->get(ScheduledTaskSubscribersRepository::class);
$newsletterBody =
json_decode(
'{
@ -123,16 +124,11 @@ class ViewInBrowserRendererTest extends \MailPoetTest {
$this->subscriber = $subscriber;
// create queue
$queue = SendingTask::create();
$queue->newsletterId = $newsletter->getId();
$queue->newsletterRenderedBody = $this->queueRenderedNewsletterWithoutTracking;
$queue->setSubscribers([$subscriber->getId()]);
$this->sendingTask = $queue->save();
$this->scheduledTask = (new ScheduledTaskFactory())->create(SendingQueue::TASK_TYPE, null);
$this->sendingQueue = (new SendingQueueFactory())->create($this->scheduledTask, $newsletter);
$this->sendingQueue->setNewsletterRenderedBody($this->queueRenderedNewsletterWithoutTracking);
$scheduledTaskSubscribersRepository->setSubscribers($this->scheduledTask, [$subscriber->getId()]);
$this->newsletterRepository->refresh($newsletter);
$scheduledTasksRepository = $this->diContainer->get(ScheduledTasksRepository::class);
$scheduledTask = $scheduledTasksRepository->findOneById($this->sendingTask->task()->id);
$this->assertInstanceOf(ScheduledTaskEntity::class, $scheduledTask);
$scheduledTasksRepository->refresh($scheduledTask);
$this->newsletter = $newsletter;
// create newsletter link associations
@ -178,7 +174,7 @@ class ViewInBrowserRendererTest extends \MailPoetTest {
$preview = false,
$this->newsletter,
$this->subscriber,
$this->sendingQueueRepository->findOneById($this->sendingTask->queue()->id)
$this->sendingQueue
);
verify($renderedBody)->stringMatchesRegExp('/Newsletter from queue/');
}
@ -189,7 +185,7 @@ class ViewInBrowserRendererTest extends \MailPoetTest {
$preview = false,
$this->newsletter,
$this->subscriber,
$this->sendingQueueRepository->findOneById($this->sendingTask->queue()->id)
$this->sendingQueue
);
verify($renderedBody)->stringContainsString('Hello, First');
verify($renderedBody)->stringContainsString(Router::NAME . '&endpoint=view_in_browser');
@ -197,7 +193,7 @@ class ViewInBrowserRendererTest extends \MailPoetTest {
public function testItRewritesLinksToRouterEndpointWhenTrackingIsEnabled() {
$this->settings->set('tracking.level', TrackingConfig::LEVEL_PARTIAL);
$queue = $this->sendingQueueRepository->findOneById($this->sendingTask->queue()->id);
$queue = $this->sendingQueue;
$this->assertInstanceOf(SendingQueueEntity::class, $queue);
$queue->setNewsletterRenderedBody($this->queueRenderedNewsletterWithTracking);
$renderedBody = $this->viewInBrowserRenderer->render(
@ -210,7 +206,7 @@ class ViewInBrowserRendererTest extends \MailPoetTest {
}
public function testItConvertsHashedLinksToUrlsWhenPreviewIsEnabledAndNewsletterWasSent() {
$queue = $this->sendingQueueRepository->findOneById($this->sendingTask->queue()->id);
$queue = $this->sendingQueue;
$this->assertInstanceOf(SendingQueueEntity::class, $queue);
$queue->setNewsletterRenderedBody($this->queueRenderedNewsletterWithTracking);
$renderedBody = $this->viewInBrowserRenderer->render(
@ -225,7 +221,7 @@ class ViewInBrowserRendererTest extends \MailPoetTest {
}
public function testRemovesOpenTrackingTagWhenPreviewIsEnabledAndNewsletterWasSent() {
$queue = $this->sendingQueueRepository->findOneById($this->sendingTask->queue()->id);
$queue = $this->sendingQueue;
$this->assertInstanceOf(SendingQueueEntity::class, $queue);
$queue->setNewsletterRenderedBody($this->queueRenderedNewsletterWithTracking);
$renderedBody = $this->viewInBrowserRenderer->render(

Some files were not shown because too many files have changed in this diff Show More