diff --git a/.gitignore b/.gitignore index 651787857..b472ff1b6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /stream.zip /stream-*.zip npm-debug.log +debug.log package.lock .phpunit.result.cache diff --git a/classes/class-connector.php b/classes/class-connector.php index d2f112396..863d9a6f8 100644 --- a/classes/class-connector.php +++ b/classes/class-connector.php @@ -294,4 +294,14 @@ function ( $value ) { public function is_dependency_satisfied() { return true; } + + /** + * Escape % characters in a string to avoid Uncaught ValueErrors in $this->log(). + * + * @param string $value The string value to be escaped. + * @return string The escaped string. + */ + public function escape_percentages( $value ) { + return str_replace( '%', '%%', $value ); + } } diff --git a/composer.json b/composer.json index 960eb9105..29ad75cd3 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "wpackagist-plugin/easy-digital-downloads": "2.9.23", "wpackagist-plugin/jetpack": "10.0", "wpackagist-plugin/user-switching": "1.5.5", + "wpackagist-plugin/wordpress-seo": "23.0", "wpackagist-theme/twentytwentythree": "^1.0", "xwp/wait-for": "^0.0.1", "yoast/phpunit-polyfills": "^1.1" @@ -77,6 +78,10 @@ "phpunit --coverage-text", "php local/scripts/make-clover-relative.php ./tests/reports/clover.xml" ], + "test-one": [ + "phpunit", + "WP_MULTISITE=1 phpunit" + ], "test-multisite": [ "WP_MULTISITE=1 phpunit --coverage-text", "php local/scripts/make-clover-relative.php ./tests/reports/clover.xml" diff --git a/composer.lock b/composer.lock index 02ca51ddd..cfdaff9a5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bea10976f57ef34a97f336ac8d1172da", + "content-hash": "8e415ec47436f6af387de15534224042", "packages": [ { "name": "composer/installers", @@ -1203,22 +1203,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.8.1", + "version": "7.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + "reference": "84ac2b2afc44e40d3e8e658a45d68d6d20437612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/84ac2b2afc44e40d3e8e658a45d68d6d20437612", + "reference": "84ac2b2afc44e40d3e8e658a45d68d6d20437612", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -1229,9 +1229,9 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1309,7 +1309,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + "source": "https://github.com/guzzle/guzzle/tree/7.9.0" }, "funding": [ { @@ -1325,20 +1325,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:35:24+00:00" + "time": "2024-07-18T11:52:56+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", "shasum": "" }, "require": { @@ -1346,7 +1346,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "type": "library", "extra": { @@ -1392,7 +1392,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.2" + "source": "https://github.com/guzzle/promises/tree/2.0.3" }, "funding": [ { @@ -1408,20 +1408,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:19:20+00:00" + "time": "2024-07-18T10:29:17+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.2", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", "shasum": "" }, "require": { @@ -1436,8 +1436,8 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1508,7 +1508,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" + "source": "https://github.com/guzzle/psr7/tree/2.7.0" }, "funding": [ { @@ -1524,7 +1524,7 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:05:35+00:00" + "time": "2024-07-18T11:15:46+00:00" }, { "name": "humanmade/mercator", @@ -8408,6 +8408,24 @@ "type": "wordpress-plugin", "homepage": "https://wordpress.org/plugins/user-switching/" }, + { + "name": "wpackagist-plugin/wordpress-seo", + "version": "23.0", + "source": { + "type": "svn", + "url": "https://plugins.svn.wordpress.org/wordpress-seo/", + "reference": "tags/23.0" + }, + "dist": { + "type": "zip", + "url": "https://downloads.wordpress.org/plugin/wordpress-seo.23.0.zip" + }, + "require": { + "composer/installers": "^1.0 || ^2.0" + }, + "type": "wordpress-plugin", + "homepage": "https://wordpress.org/plugins/wordpress-seo/" + }, { "name": "wpackagist-theme/twentytwentythree", "version": "1.5", @@ -8538,5 +8556,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/connectors/class-connector-gravityforms.php b/connectors/class-connector-gravityforms.php index 8485b96ed..034bbcda0 100644 --- a/connectors/class-connector-gravityforms.php +++ b/connectors/class-connector-gravityforms.php @@ -221,20 +221,23 @@ public function register() { * @param bool $is_new Is this a new form?. */ public function callback_gform_after_save_form( $form, $is_new ) { - $title = $form['title']; - $id = $form['id']; + $id = $form['id']; $this->log( sprintf( /* translators: %1$s a form title, %2$s a status (e.g. "Contact Form", "created") */ __( '"%1$s" form %2$s', 'stream' ), - $title, - $is_new ? esc_html__( 'created', 'stream' ) : esc_html__( 'updated', 'stream' ) + $this->get_form_title_for_message( $form ), + $this->get_status_for_message( + $is_new, + esc_html__( 'created', 'stream' ), + esc_html__( 'updated', 'stream' ) + ) ), array( 'action' => $is_new, 'id' => $id, - 'title' => $title, + 'title' => $form['title'], ), $id, 'forms', @@ -258,9 +261,13 @@ public function callback_gform_pre_confirmation_save( $confirmation, $form, $is_ sprintf( /* translators: %1$s: a confirmation name, %2$s: a status, %3$s: a form title (e.g. "Email", "created", "Contact Form") */ __( '"%1$s" confirmation %2$s for "%3$s"', 'stream' ), - $confirmation['name'], - $is_new ? esc_html__( 'created', 'stream' ) : esc_html__( 'updated', 'stream' ), - $form['title'] + $this->get_name_for_message( $confirmation ), + $this->get_status_for_message( + $is_new, + esc_html__( 'created', 'stream' ), + esc_html__( 'updated', 'stream' ) + ), + $this->get_form_title_for_message( $form ) ), array( 'is_new' => $is_new, @@ -291,9 +298,13 @@ public function callback_gform_pre_notification_save( $notification, $form, $is_ sprintf( /* translators: %1$s: a notification name, %2$s: a status, %3$s: a form title (e.g. "Email", "created", "Contact Form") */ __( '"%1$s" notification %2$s for "%3$s"', 'stream' ), - $notification['name'], - $is_new ? esc_html__( 'created', 'stream' ) : esc_html__( 'updated', 'stream' ), - $form['title'] + $this->get_name_for_message( $notification ), + $this->get_status_for_message( + $is_new, + esc_html__( 'created', 'stream' ), + esc_html__( 'updated', 'stream' ) + ), + $this->get_form_title_for_message( $form ) ), array( 'is_update' => $is_new, @@ -318,8 +329,8 @@ public function callback_gform_pre_notification_deleted( $notification, $form ) sprintf( /* translators: %1$s: a notification name, %2$s: a form title (e.g. "Email", "Contact Form") */ __( '"%1$s" notification deleted from "%2$s"', 'stream' ), - $notification['name'], - $form['title'] + $this->get_name_for_message( $notification ), + $this->get_form_title_for_message( $form ) ), array( 'form_id' => $form['id'], @@ -342,8 +353,8 @@ public function callback_gform_pre_confirmation_deleted( $confirmation, $form ) sprintf( /* translators: %1$s: a confirmation name, %2$s: a form title (e.g. "Email", "Contact Form") */ __( '"%1$s" confirmation deleted from "%2$s"', 'stream' ), - $confirmation['name'], - $form['title'] + $this->get_name_for_message( $confirmation ), + $this->get_form_title_for_message( $form ) ), array( 'form_id' => $form['id'], @@ -367,9 +378,13 @@ public function callback_gform_confirmation_status( $confirmation, $form, $is_ac sprintf( /* translators: %1$s: a confirmation name, %2$s: a status, %3$s: a form title (e.g. "Email", "activated", "Contact Form") */ __( '"%1$s" confirmation %2$s from "%3$s"', 'stream' ), - $confirmation['name'], - $is_active ? esc_html__( 'activated', 'stream' ) : esc_html__( 'deactivated', 'stream' ), - $form['title'] + $this->get_name_for_message( $confirmation ), + $this->get_status_for_message( + $is_active, + esc_html__( 'activated', 'stream' ), + esc_html__( 'deactivated', 'stream' ) + ), + $this->get_form_title_for_message( $form ) ), array( 'form_id' => $form['id'], @@ -394,9 +409,13 @@ public function callback_gform_notification_status( $notification, $form, $is_ac sprintf( /* translators: %1$s: a notification name, %2$s: a status, %3$s: a form title (e.g. "Email", "activated", "Contact Form") */ __( '"%1$s" notification %2$s from "%3$s"', 'stream' ), - $notification['name'], - $is_active ? esc_html__( 'activated', 'stream' ) : esc_html__( 'deactivated', 'stream' ), - $form['title'] + $this->get_name_for_message( $notification ), + $this->get_status_for_message( + $is_active, + esc_html__( 'activated', 'stream' ), + esc_html__( 'deactivated', 'stream' ) + ), + $this->get_form_title_for_message( $form ) ), array( 'form_id' => $form['id'], @@ -514,7 +533,11 @@ public function check_rg_gforms_key( $old_value, $new_value ) { sprintf( /* translators: %s: a status (e.g. "updated") */ __( 'Gravity Forms license key %s', 'stream' ), - $is_update ? esc_html__( 'updated', 'stream' ) : esc_html__( 'deleted', 'stream' ) + $this->get_status_for_message( + $is_update, + esc_html__( 'updated', 'stream' ), + esc_html__( 'deleted', 'stream' ) + ) ), compact( 'option', 'old_value', 'new_value' ), null, @@ -755,8 +778,8 @@ public function callback_gform_update_status( $lead_id, $status, $prev = '' ) { /* translators: %1$d: an ID, %2$s: a status, %3$s: a form title (e.g. "42", "activated", "Contact Form") */ __( 'Lead #%1$d %2$s on "%3$s" form', 'stream' ), $lead_id, - $actions[ $status ], - $form['title'] + $this->escape_percentages( $actions[ $status ] ), + $this->get_form_title_for_message( $form ) ), array( 'lead_id' => $lead_id, @@ -780,18 +803,21 @@ public function callback_gform_update_status( $lead_id, $status, $prev = '' ) { * @param string $status Status. */ public function callback_gform_update_is_read( $lead_id, $status ) { - $lead = $this->get_lead( $lead_id ); - $form = $this->get_form( $lead['form_id'] ); - $status = ( ! empty( $status ) ) ? esc_html__( 'read', 'stream' ) : esc_html__( 'unread', 'stream' ); + $lead = $this->get_lead( $lead_id ); + $form = $this->get_form( $lead['form_id'] ); $this->log( sprintf( /* translators: %1$d: a lead ID, %2$s: a status, %3$s: a form ID, %4$s: a form title (e.g. "42", "unread", "Contact Form") */ __( 'Entry #%1$d marked as %2$s on form #%3$d ("%4$s")', 'stream' ), $lead_id, - $status, + $this->get_status_for_message( + ! empty( $status ), + esc_html__( 'read', 'stream' ), + esc_html__( 'unread', 'stream' ) + ), $form['id'], - $form['title'] + $this->get_form_title_for_message( $form ) ), array( 'lead_id' => $lead_id, @@ -814,19 +840,23 @@ public function callback_gform_update_is_read( $lead_id, $status ) { * @param int $status Status. */ public function callback_gform_update_is_starred( $lead_id, $status ) { - $lead = $this->get_lead( $lead_id ); - $form = $this->get_form( $lead['form_id'] ); - $status = ( ! empty( $status ) ) ? esc_html__( 'starred', 'stream' ) : esc_html__( 'unstarred', 'stream' ); - $action = $status; + $lead = $this->get_lead( $lead_id ); + $form = $this->get_form( $lead['form_id'] ); + $status_for_message = $this->get_status_for_message( + ! empty( $status ), + esc_html__( 'starred', 'stream' ), + esc_html__( 'unstarred', 'stream' ) + ); + $action = $status_for_message; $this->log( sprintf( /* translators: %1$d: an ID, %2$s: a status, %3$d: a form title (e.g. "42", "starred", "Contact Form") */ __( 'Entry #%1$d %2$s on form #%3$d ("%4$s")', 'stream' ), $lead_id, - $status, + $status_for_message, // This has been escaped above. $form['id'], - $form['title'] + $this->get_form_title_for_message( $form ) ), array( 'lead_id' => $lead_id, @@ -945,8 +975,8 @@ public function log_form_action( $form_id, $action ) { /* translators: %1$d: an ID, %2$s: a form title, %3$s: a status (e.g. "42", "Contact Form", "Activated") */ __( 'Form #%1$d ("%2$s") %3$s', 'stream' ), $form_id, - $form['title'], - strtolower( $actions[ $action ] ) + $this->get_form_title_for_message( $form ), + strtolower( $this->escape_percentages( $actions[ $action ] ) ) ), array( 'form_id' => $form_id, @@ -976,4 +1006,47 @@ private function get_lead( $lead_id ) { private function get_form( $form_id ) { return \GFFormsModel::get_form_meta( $form_id ); } + + /** + * Get the name from an associative array with percentages escaped. + * To be used when calling $this->log(). + * + * @param array $data_array The array with a 'name' key to be escaped. + * @return string The name value with percentages escaped. + */ + private function get_name_for_message( $data_array ) { + if ( empty( $data_array['name'] ) ) { + return $this->escape_percentages( __( 'This does not have a name key', 'stream' ) ); + } + + return $this->escape_percentages( $data_array['name'] ); + } + + /** + * Get the form title from a form array with percentages escaped. + * To be used when calling $this->log(). + * + * @param array $form The form data array. + * @return string The title value with percentages escaped. + */ + private function get_form_title_for_message( $form ) { + if ( empty( $form['title'] ) ) { + return $this->escape_percentages( __( 'This does not have a title key', 'stream' ) ); + } + + return $this->escape_percentages( $form['title'] ); + } + + /** + * Get the status to use in a message with percentages in translation escaped. + * + * @param bool $conditional Whether or not it's a new form. + * @param string $true_string The string to return when the conditional is true. + * @param string $false_string The string to return when the conditional is false. + * @return string The status with percentages escaped. + */ + private function get_status_for_message( $conditional, $true_string, $false_string ) { + $status = $conditional ? $true_string : $false_string; + return $this->escape_percentages( $status ); + } } diff --git a/connectors/class-connector-wordpress-seo.php b/connectors/class-connector-wordpress-seo.php index 573f653b7..7c52a0864 100644 --- a/connectors/class-connector-wordpress-seo.php +++ b/connectors/class-connector-wordpress-seo.php @@ -419,9 +419,9 @@ private function meta( $object_id, $meta_key, $meta_value ) { sprintf( /* translators: %1$s: a meta field title, %2$s: a post title, %3$s: a post type (e.g. "Description", "Hello World", "Post") */ __( 'Updated "%1$s" of "%2$s" %3$s', 'stream' ), - $field['title'], - $post->post_title, - $post_type_label + $this->escape_percentages( $field['title'] ), + $this->escape_percentages( $post->post_title ), + $this->escape_percentages( $post_type_label ) ), array( 'meta_key' => $meta_key, diff --git a/local/public/wp-config.php b/local/public/wp-config.php index 42734f680..ae947c5ec 100644 --- a/local/public/wp-config.php +++ b/local/public/wp-config.php @@ -25,6 +25,7 @@ $table_prefix = 'wp_'; define( 'WP_DEBUG', true ); +define( 'WP_DEBUG_LOG', true ); define( 'JETPACK_DEV_DEBUG', true ); // Keep the wp-contents outside of WP core directory. diff --git a/package.json b/package.json index 554454068..b47c791ba 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "test": "npm-run-all test:*", "test:php": "npm run cli -- composer test --working-dir=wp-content/plugins/stream-src", "test:php-multisite": "npm run cli -- composer test-multisite --working-dir=wp-content/plugins/stream-src", + "test:php-one": "npm run cli -- composer test-one --working-dir=wp-content/plugins/stream-src --", "test-report": "npm run cli -- composer test-report --working-dir=wp-content/plugins/stream-src", "build-containers": "docker compose --file docker-compose.build.yml build", "push-containers": "docker compose --file docker-compose.build.yml push", diff --git a/phpunit.xml b/phpunit.xml index f37a7bf54..1254c9721 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,7 +10,7 @@ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e232ec3a8..94575196c 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -104,6 +104,7 @@ function () { define( 'EDD_USE_PHP_SESSIONS', false ); define( 'WP_USE_THEMES', false ); activate_plugin( 'easy-digital-downloads/easy-digital-downloads.php' ); +activate_plugin( 'wordpress-seo/wp-seo.php' ); wp_stream_install_edd(); require __DIR__ . '/testcase.php'; diff --git a/tests/testcase.php b/tests/testcase.php index 6dd1ed89b..95c89cef1 100644 --- a/tests/testcase.php +++ b/tests/testcase.php @@ -16,6 +16,13 @@ class WP_StreamTestCase extends \WP_Ajax_UnitTestCase { */ protected $action_prefix = 'wp_stream_test_'; + /** + * Holds the mocked class. + * + * @var MockBuilder + */ + protected $mock; + /** * PHP unit setup function * diff --git a/tests/tests/connectors/test-class-connector-wordpress-seo.php b/tests/tests/connectors/test-class-connector-wordpress-seo.php new file mode 100644 index 000000000..246b53bd9 --- /dev/null +++ b/tests/tests/connectors/test-class-connector-wordpress-seo.php @@ -0,0 +1,84 @@ +plugin->connectors->unload_connectors(); + + // Make partial of Connector_WordPress_SEO class, with mocked "log" function. + $this->mock = $this->getMockBuilder( Connector_WordPress_SEO::class ) + ->onlyMethods( array( 'log' ) ) + ->getMock(); + + // Register connector. + $this->mock->register(); + } + + /** + * Confirm that WordPress SEO is installed and active. + */ + public function test_wordpress_seo_installed_and_activated() { + $this->assertTrue( defined( 'YOAST_ENVIRONMENT' ) ); + } + + /** + * Tests "added_post_meta" callback function. + * callback_added_post_meta( $meta_id, $object_id, $meta_key, $meta_value ) + */ + public function test_callback_added_post_meta() { + + // Set expected calls for the Mock. + $this->mock->expects( $this->once() ) + ->method( 'log' ) + ->with( + $this->equalTo( + __( 'Updated "SEO title" of "Test post %%!" Post', 'stream' ) + ), + $this->equalTo( + array( + 'meta_key' => $this->title_meta_key, + 'meta_value' => 'Test meta %!', + 'post_type' => 'post', + ) + ), + $this->greaterThan( 0 ), + 'wpseo_meta', + 'updated' + ); + + // Create post for later use. + $post_id = wp_insert_post( + array( + 'post_title' => 'Test post %!', + 'post_content' => 'Lorem ipsum dolor...', + 'post_status' => 'publish', + ) + ); + + update_post_meta( $post_id, $this->title_meta_key, 'Test meta %!' ); + + // Confirm callback execution. + $this->assertGreaterThan( 0, did_action( $this->action_prefix . 'callback_added_post_meta' ) ); + } +} diff --git a/tests/tests/test-class-connector.php b/tests/tests/test-class-connector.php index 2edfd021b..c9342359d 100644 --- a/tests/tests/test-class-connector.php +++ b/tests/tests/test-class-connector.php @@ -235,4 +235,17 @@ public function test_get_changed_keys() { public function test_is_dependency_satisfied() { $this->assertTrue( $this->connector->is_dependency_satisfied() ); } + + /** + * Test that percentages are escaped. + * + * @return void + */ + public function test_escape_percentages() { + $escaped_value = $this->connector->escape_percentages( 'This is a message with a % sign' ); + $this->assertEquals( + 'This is a message with a %% sign', + $escaped_value + ); + } }