diff --git a/WpMailCatcher.php b/WpMailCatcher.php index 3f61aff..c1e4cf4 100644 --- a/WpMailCatcher.php +++ b/WpMailCatcher.php @@ -6,7 +6,7 @@ Domain Path: /languages Description: Logging your mail will stop you from ever losing your emails again! This fast, lightweight plugin (under 140kb in size!) is also useful for debugging or backing up your messages. Author: James Ward -Version: 2.1.7 +Version: 2.1.8 Author URI: https://jamesward.io Donate link: https://paypal.me/jamesmward */ diff --git a/build/grunt/package-lock.json b/build/grunt/package-lock.json index 45c2839..086ecd0 100644 --- a/build/grunt/package-lock.json +++ b/build/grunt/package-lock.json @@ -1,6 +1,6 @@ { "name": "WpMailCatcher", - "version": "2.1.4", + "version": "2.1.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/build/grunt/package.json b/build/grunt/package.json index f628405..e42514b 100644 --- a/build/grunt/package.json +++ b/build/grunt/package.json @@ -1,6 +1,6 @@ { "name": "WpMailCatcher", - "version": "2.1.7", + "version": "2.1.8", "lang_po_directory": "../../languages", "build_directory": "./..", "dist_directory": "../../assets", diff --git a/entrypoint.sh b/entrypoint.sh index 318b753..8ffa3a2 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,7 +4,7 @@ export DB_DATABASE=wordpress export DB_USERNAME=wp_mail_catcher export DB_PASSWORD=password export PHP_VERSION=8.0 -export WP_VERSION=6.4.1 +export WP_VERSION=6.5.3 CMD=$1 diff --git a/languages/WpMailCatcher.pot b/languages/WpMailCatcher.pot index 0f353cf..1eb3479 100644 --- a/languages/WpMailCatcher.pot +++ b/languages/WpMailCatcher.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: WpMailCatcher 2.1.4\n" +"Project-Id-Version: WpMailCatcher 2.1.8\n" "Report-Msgid-Bugs-To: wordpress@jamesward.io\n" -"POT-Creation-Date: 2023-11-09 20:31+0000\n" +"POT-Creation-Date: 2024-05-29 19:52+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -131,7 +131,7 @@ msgid "" " to perform the upgrade." msgstr "" -#: ../../src/Views/Log.php:47 +#: ../../src/Views/Log.php:50 #, php-format msgid "" "You have over %s messages stored and auto-" @@ -141,27 +141,27 @@ msgid "" " auto-delete or delete some logs." msgstr "" -#: ../../src/Views/Log.php:62 +#: ../../src/Views/Log.php:65 msgid "New Message" msgstr "" -#: ../../src/Views/Log.php:67 ../../src/Views/Log.php:77 +#: ../../src/Views/Log.php:70 ../../src/Views/Log.php:80 msgid "Export all messages" msgstr "" -#: ../../src/Views/Log.php:88 +#: ../../src/Views/Log.php:91 msgid "All" msgstr "" -#: ../../src/Views/Log.php:96 +#: ../../src/Views/Log.php:99 msgid "Successful" msgstr "" -#: ../../src/Views/Log.php:104 +#: ../../src/Views/Log.php:107 msgid "Failed" msgstr "" -#: ../../src/Views/Log.php:118 +#: ../../src/Views/Log.php:121 msgid "Search Logs" msgstr "" @@ -290,20 +290,20 @@ msgstr "" msgid "Yes - delete messages that are over %s old" msgstr "" -#: ../../src/Views/Settings.php:141 +#: ../../src/Views/Settings.php:144 #, php-format msgid "Will next run in: %s. insert($wpdb->prefix . GeneralHelper::$tableName, $transformFunc($args)); + $transformedArgs = $transformFunc($args); + $userFilteredArgs = apply_filters( + GeneralHelper::$actionNameSpace . '_before_success_log_save', + // Only allow certain values to be changed via filters/hooks + array_intersect_key($transformedArgs, array_fill_keys(Logs::$whitelistedColumns, null)) + ); + + if ($userFilteredArgs === false) { + return []; + } + + $wpdb->insert($wpdb->prefix . GeneralHelper::$tableName, array_merge($transformedArgs, $userFilteredArgs)); Cache::flush(); @@ -53,18 +64,38 @@ public function saveError(string $error) global $wpdb; + $log = Logs::getFirst(['post__in' => $this->id]); + $log['status'] = 0; + $log['error'] = $error; + $log['time'] = $log['timestamp']; + + $transformedArgs = apply_filters( + GeneralHelper::$actionNameSpace . '_before_error_log_save', + // Only allow certain values to be changed via filters/hooks + array_intersect_key($log, array_fill_keys(Logs::$whitelistedColumns, null)) + ); + + if ($transformedArgs === false) { + // Because of the way wp_mail hook works the log needs to be saved + // before appending any error that happened to it. As a result we need + // to delete the inital entry from the db rather than just `return` + Logs::delete($this->id); + return; + } + + if (!$transformedArgs) { + $transformedArgs = $log; + } + $wpdb->update( $wpdb->prefix . GeneralHelper::$tableName, - [ - 'status' => 0, - 'error' => $error, - ], + array_merge($transformedArgs, ['status' => 0]), ['id' => $this->id] ); Cache::flush(); - do_action(GeneralHelper::$actionNameSpace . '_mail_failed', Logs::getFirst(['post__in' => $this->id])); + do_action(GeneralHelper::$actionNameSpace . '_mail_failed', $log); } public function saveIsHtml($contentType) @@ -94,7 +125,7 @@ public function saveIsHtml($contentType) * Convert attachment ids or urls into a format to be usable * by the logs * - * @param array | string $attachments either array of attachment ids or their urls + * @param array | string $attachments either array of attachment ids or their urls * * @return array [id, url] of attachments */ diff --git a/src/Models/Logs.php b/src/Models/Logs.php index c62ba6f..5197747 100644 --- a/src/Models/Logs.php +++ b/src/Models/Logs.php @@ -6,6 +6,11 @@ class Logs { + // The db columns that can be set when logs are updated via filters/hooks + public static $whitelistedColumns = [ + 'email_to', 'subject', 'message', 'error', 'time', 'backtrace_segment' + ]; + // Need to set null because we're using array_intersect_key public static $whitelistedParams = [ 'orderby' => null, diff --git a/testing/tests/TestEmails.php b/testing/tests/TestEmails.php index 7be2f7b..c6882d6 100644 --- a/testing/tests/TestEmails.php +++ b/testing/tests/TestEmails.php @@ -10,12 +10,22 @@ public function setUp(): void Logs::truncate(); } - public function testMail() + private function isTimeBetwen($time, $compareToTime, $threshold) { - $to = 'test@test.com'; + $upperBound = $compareToTime + $threshold; + $lowerBound = $compareToTime - $threshold; + return $time < $upperBound && $time > $lowerBound; + } + + // TODO: Tidy method up, make more generic rather than just setting current args + // needed by other tests + private function sendAndAssertMail($to, $additionalHeaders = [], $isHtml = false) + { + // FIXME: Need a better way to mock `time()` + $time = time(); + $acceptableTimeThreshold = 5; $subject = 'subject'; $message = 'message'; - $additionalHeaders = [GeneralHelper::$htmlEmailHeader, 'cc: test1@test.com']; $imgAttachmentId = $this->factory()->attachment->create_upload_object(__DIR__ . '/../assets/img-attachment.png'); $pdfAttachmentId = $this->factory()->attachment->create_upload_object(__DIR__ . '/../assets/pdf-attachment.pdf'); @@ -31,10 +41,12 @@ public function testMail() $this->assertEquals($to, $emailLogs[0]['email_to']); $this->assertEquals($subject, $emailLogs[0]['subject']); $this->assertEquals($message, $emailLogs[0]['message']); - $this->assertTrue($emailLogs[0]['is_html']); + $this->assertTrue($this->isTimeBetwen($emailLogs[0]['timestamp'], $time, $acceptableTimeThreshold)); + $this->assertEquals($isHtml, $emailLogs[0]['is_html']); - $this->assertEquals($additionalHeaders[0], $emailLogs[0]['additional_headers'][0]); - $this->assertEquals($additionalHeaders[1], $emailLogs[0]['additional_headers'][1]); + foreach ($additionalHeaders as $index => $additionalHeader) { + $this->assertEquals($additionalHeader, $emailLogs[0]['additional_headers'][$index]); + } $this->assertEquals($imgAttachmentId, $emailLogs[0]['attachments'][0]['id']); $this->assertEquals(wp_get_attachment_url($imgAttachmentId), $emailLogs[0]['attachments'][0]['url']); @@ -48,13 +60,13 @@ public function testMail() public function testCorrectTos() { - wp_mail('test@test.com', 'subject', 'message'); + $this->sendAndAssertMail('test@test.com', [GeneralHelper::$htmlEmailHeader, 'cc: test1@test.com'], true); $this->assertTrue(Logs::getFirst()['status']); } public function testIncorrectTos() { - wp_mail('testtest.com', 'subject', 'message'); + $this->sendAndAssertMail('testtest.com'); $this->assertFalse(Logs::getFirst()['status']); } diff --git a/testing/tests/TestLogFunctions.php b/testing/tests/TestLogFunctions.php index e296a36..e242122 100644 --- a/testing/tests/TestLogFunctions.php +++ b/testing/tests/TestLogFunctions.php @@ -485,4 +485,119 @@ public function testCanDecodeAsciQuotedEncodedSubjectLine() preg_replace('/\s+/', '', $expectedOutput) ); } + + public function testCanAlterSuccessfulLogBeforeSavingViaFilter() + { + $beforeTo = 'before@test.com'; + $beforeSubject = 'Before subject'; + + $afterTo = 'after@test.com'; + $afterSubject = 'After subject'; + $afterTime = 123; + $afterBacktrace = 'Hello world'; + $afterMessage = 'My new message'; + + $filterName = GeneralHelper::$actionNameSpace . '_before_success_log_save'; + + $func = function ($log) use ($afterTo, $afterSubject, $afterMessage, $afterTime, $afterBacktrace) { + $log['email_to'] = $afterTo; + $log['subject'] = $afterSubject; + $log['message'] = $afterMessage; + $log['time'] = $afterTime; + $log['backtrace_segment'] = $afterBacktrace; + return $log; + }; + + add_filter($filterName, $func); + + wp_mail($beforeTo, $beforeSubject, 'Hello'); + + $emailLog = Logs::getFirst(); + + $this->assertEquals($afterTo, $emailLog['email_to']); + $this->assertEquals($afterSubject, $emailLog['subject']); + $this->assertEquals($afterMessage, $emailLog['message']); + $this->assertEquals($afterTime, $emailLog['timestamp']); + $this->assertEquals($afterBacktrace, $emailLog['backtrace_segment']); + + remove_filter($filterName, $func); + } + + public function testCanStopSuccessfulLogFromSavingViaFilter() + { + $filterName = GeneralHelper::$actionNameSpace . '_before_success_log_save'; + + $func = function ($log) { + return false; + }; + + add_filter($filterName, $func); + + wp_mail('test@test.com', 'Subject', 'Hello'); + + $emailLogs = Logs::get(); + $this->assertCount(0, $emailLogs); + + remove_filter($filterName, $func); + } + + public function testCanAlterErroredLogBeforeSavingViaFilter() + { + // Use invalid email address to trigger an error + $beforeTo = 'beforetest.com'; + $beforeSubject = 'Before subject'; + + $afterTo = 'after@test.com'; + $afterSubject = 'After subject'; + $afterErrorMessage = 'Something went wrong'; + $afterMessage = 'My new message'; + $afterTime = 123; + $afterBacktrace = 'Hello world'; + + $filterName = GeneralHelper::$actionNameSpace . '_before_error_log_save'; + + $func = function ($log) use ($afterTo, $afterSubject, $afterErrorMessage, $afterMessage, $afterTime, $afterBacktrace) { + $log['email_to'] = $afterTo; + $log['subject'] = $afterSubject; + $log['message'] = $afterMessage; + $log['error'] = $afterErrorMessage; + $log['time'] = $afterTime; + $log['backtrace_segment'] = $afterBacktrace; + return $log; + }; + + add_filter($filterName, $func); + + wp_mail($beforeTo, $beforeSubject, 'Hello'); + + $emailLog = Logs::getFirst(); + + $this->assertEquals($afterTo, $emailLog['email_to']); + $this->assertEquals($afterSubject, $emailLog['subject']); + $this->assertEquals($afterErrorMessage, $emailLog['error']); + $this->assertEquals($afterMessage, $emailLog['message']); + $this->assertEquals($afterTime, $emailLog['timestamp']); + $this->assertEquals($afterBacktrace, $emailLog['backtrace_segment']); + + remove_filter($filterName, $func); + } + + public function testCanStopErroredLogFromSavingViaFilter() + { + $filterName = GeneralHelper::$actionNameSpace . '_before_error_log_save'; + + $func = function ($log) { + return false; + }; + + add_filter($filterName, $func); + + // Use invalid email address to trigger an error + wp_mail('testtest.com', 'Subject', 'Hello'); + + $emailLogs = Logs::get(); + $this->assertCount(0, $emailLogs); + + remove_filter($filterName, $func); + } }