diff --git a/assets/js/src/front_link_checker.js b/assets/js/src/front_link_checker.js index 0e3f62c0..9748ce63 100644 --- a/assets/js/src/front_link_checker.js +++ b/assets/js/src/front_link_checker.js @@ -119,13 +119,17 @@ const getRenderedLinks = () => { }); } - // Look for all script tags with '__iawmlf-post-loop-links' class - const loopLinks = document.querySelectorAll('script.__iawmlf-post-loop-links'); + // Look for all loop data nodes with '__iawmlf-post-loop-links' class + const loopLinks = document.querySelectorAll('.__iawmlf-post-loop-links[data-iawmlf-links]'); - // Get the links from the script tag content + // Get the links from the loop data attribute. loopLinks.forEach((loopLink) => { - const links = JSON.parse(loopLink.textContent); - addLinks(links); + try { + const links = JSON.parse(loopLink.getAttribute('data-iawmlf-links')); + addLinks(links); + } catch (e) { + // Do nothing. + } }); diff --git a/internet-archive-wayback-machine-link-fixer.php b/internet-archive-wayback-machine-link-fixer.php index fc3454bf..20fbcfdb 100644 --- a/internet-archive-wayback-machine-link-fixer.php +++ b/internet-archive-wayback-machine-link-fixer.php @@ -3,7 +3,7 @@ * The wayback-link-fixer bootstrap file. * * @since 1.0.0 - * @version 1.4.0 + * @version 1.4.1 * @author Internet Archive * @license GPL-3.0-or-later * @@ -12,7 +12,7 @@ * @wordpress-plugin * Plugin Name: Internet Archive Wayback Machine Link Fixer * Description: This plugin scans your content for links, replacing broken ones with archived versions from the Wayback Machine. It also features Auto Archiving, which automatically creates snapshots of your own pages and any other links on your site that aren’t yet archived, ensuring long-term accessibility. - * Version: 1.4.0 + * Version: 1.4.1 * Requires at least: 6.4 * Tested up to: 7.0 * Requires PHP: 7.4 @@ -30,7 +30,7 @@ define( 'IAWMLF_BASENAME', plugin_basename( __FILE__ ) ); define( 'IAWMLF_PATH', plugin_dir_path( __FILE__ ) ); define( 'IAWMLF_URL', plugin_dir_url( __FILE__ ) ); -define( 'IAWMLF_VERSION', '1.4.0' ); +define( 'IAWMLF_VERSION', '1.4.1' ); define( 'IAWMLF_MINIMUM_VERSIONS', array( diff --git a/package-lock.json b/package-lock.json index 7895986b..45d7ca53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wayback-link-fixer", - "version": "1.4.0", + "version": "1.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wayback-link-fixer", - "version": "1.4.0", + "version": "1.4.1", "license": "GPL-2.0-or-later", "devDependencies": { "@csstools/postcss-sass": "^5.0.1", @@ -5265,7 +5265,8 @@ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", "dev": true, - "funding": [{ + "funding": [ + { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, @@ -5514,7 +5515,8 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, - "funding": [{ + "funding": [ + { "type": "github", "url": "https://github.com/sponsors/feross" }, @@ -5674,7 +5676,8 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "dev": true, - "funding": [{ + "funding": [ + { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, @@ -5714,7 +5717,8 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, - "funding": [{ + "funding": [ + { "type": "github", "url": "https://github.com/sponsors/feross" }, @@ -5880,7 +5884,8 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz", "integrity": "sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==", "dev": true, - "funding": [{ + "funding": [ + { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, @@ -5957,10 +5962,12 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, - "funding": [{ - "type": "individual", - "url": "https://paulmillr.com/funding/" - }], + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -6009,10 +6016,12 @@ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, - "funding": [{ - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - }], + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "engines": { "node": ">=8" } @@ -7117,10 +7126,12 @@ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, - "funding": [{ - "type": "github", - "url": "https://github.com/sponsors/fb55" - }] + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] }, "node_modules/domexception": { "version": "2.0.1", @@ -8620,10 +8631,12 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", "dev": true, - "funding": [{ - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - }], + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "engines": { "node": ">=4.0" }, @@ -9248,7 +9261,8 @@ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", "dev": true, - "funding": [{ + "funding": [ + { "type": "github", "url": "https://github.com/sponsors/mdevils" }, @@ -9407,7 +9421,8 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, - "funding": [{ + "funding": [ + { "type": "github", "url": "https://github.com/sponsors/feross" }, @@ -11919,10 +11934,12 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true, - "funding": [{ - "type": "github", - "url": "https://github.com/sponsors/ai" - }], + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -12996,7 +13013,8 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, - "funding": [{ + "funding": [ + { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, @@ -13710,7 +13728,8 @@ "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", "dev": true, - "funding": [{ + "funding": [ + { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, @@ -14041,7 +14060,8 @@ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, - "funding": [{ + "funding": [ + { "type": "github", "url": "https://github.com/sponsors/feross" }, @@ -14707,7 +14727,8 @@ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, - "funding": [{ + "funding": [ + { "type": "github", "url": "https://github.com/sponsors/feross" }, @@ -14762,7 +14783,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, - "funding": [{ + "funding": [ + { "type": "github", "url": "https://github.com/sponsors/feross" }, @@ -16522,7 +16544,8 @@ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, - "funding": [{ + "funding": [ + { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, @@ -30394,4 +30417,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 04aca5aa..9b75b345 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wayback-link-fixer", - "version": "1.4.0", + "version": "1.4.1", "description": "A WordPress plugin that scans content for links, replacing broken ones with archived versions from the Wayback Machine. It also features Auto Archiving, creating snapshots of your own pages and other site links that aren’t yet archived. Uses Action Scheduler for background tasks.", "author": { "name": "WordPress.com Special Projects Team", diff --git a/readme.txt b/readme.txt index 2f9a2440..be0690f5 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: waybackmachineplugin, wpspecialprojects, cagrimmett, glynnquelch Tags: wayback machine, internet archive, broken links, archive links Requires at least: 6.4 Tested up to: 6.9 -Stable tag: 1.4.0 +Stable tag: 1.4.1 Requires PHP: 7.4 License: GPL-3.0-or-later License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -116,6 +116,9 @@ The Internet Archive is a non-profit organization dedicated to preserving digita == Changelog == += 1.4.1 = +* Fix: link data span now survives themes that wrap post content in `wp_kses_post` (previously the JSON could leak as visible text on category and archive templates). + = 1.4.0 = * Changes to how we store link information in posts, now uses an escaped `"; + // Encode with all JSON_HEX_* flags. + $json = wp_json_encode( $links, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); + $html_data = ''; - return $html_data . $block_content; + return $block_content . $html_data; } /** diff --git a/tests/WP_Post/Test_WP_Post_Controller.php b/tests/WP_Post/Test_WP_Post_Controller.php index b1a6edea..b6d76f4b 100644 --- a/tests/WP_Post/Test_WP_Post_Controller.php +++ b/tests/WP_Post/Test_WP_Post_Controller.php @@ -653,14 +653,11 @@ public function test_excluded_link_not_included_in_render_block_data(): void { // Render the block. $rendered = do_blocks( $GLOBALS['post']->post_content ); - // The script tag should be present (we still have one non-excluded link). + // The link data span should be present (we still have one non-excluded link). $this->assertStringContainsString( '__iawmlf-post-loop-links', $rendered ); - // Extract the JSON from the script tag. - preg_match( "/]*class='__iawmlf-post-loop-links'[^>]*>(.*?)<\/script>/s", $rendered, $matches ); - $this->assertNotEmpty( $matches, 'Should find the script tag in rendered output.' ); - - $links_data = json_decode( $matches[1], true ); + $links_data = $this->extract_link_data_from_rendered( $rendered ); + $this->assertNotNull( $links_data, 'Should find the link data span in rendered output.' ); $this->assertIsArray( $links_data ); // Collect all hrefs from the links data. @@ -715,14 +712,11 @@ public function test_excluded_link_filtered_from_render_block_data_with_mixed_li // Render the block. $rendered = do_blocks( $GLOBALS['post']->post_content ); - // The script tag should be present (we still have one non-excluded link). + // The link data span should be present (we still have one non-excluded link). $this->assertStringContainsString( '__iawmlf-post-loop-links', $rendered ); - // Extract the JSON from the script tag. - preg_match( "/]*class='__iawmlf-post-loop-links'[^>]*>(.*?)<\/script>/s", $rendered, $matches ); - $this->assertNotEmpty( $matches, 'Should find the script tag in rendered output.' ); - - $links_data = json_decode( $matches[1], true ); + $links_data = $this->extract_link_data_from_rendered( $rendered ); + $this->assertNotNull( $links_data, 'Should find the link data span in rendered output.' ); $this->assertIsArray( $links_data ); // Should only have 1 link (the non-excluded one). @@ -874,4 +868,272 @@ public function test_on_save_excluded_auto_archiver_post_not_queued(): void { // Clean up. \delete_option( Settings::AUTO_ARCHIVER_EXCLUDED_POSTS ); } + + /** + * Extract the link data array from rendered HTML by parsing the span's + * data-iawmlf-links attribute via DOMDocument (mirrors browser behaviour). + * + * @param string $rendered The rendered HTML. + * + * @return array>|null + */ + private function extract_link_data_from_rendered( string $rendered ): ?array { + $dom = new \DOMDocument(); + $prev = libxml_use_internal_errors( true ); + $dom->loadHTML( '' . $rendered . '', LIBXML_NOERROR | LIBXML_NOWARNING ); + libxml_clear_errors(); + libxml_use_internal_errors( $prev ); + + $xpath = new \DOMXPath( $dom ); + $nodes = $xpath->query( "//span[contains(concat(' ', normalize-space(@class), ' '), ' __iawmlf-post-loop-links ')]" ); + + if ( ! $nodes || 0 === $nodes->length ) { + return null; + } + + $node = $nodes->item( 0 ); + $json = $node->getAttribute( 'data-iawmlf-links' ); + $data = json_decode( $json, true ); + + return is_array( $data ) ? $data : null; + } + + /** + * Build a published post with the given anchor markup and return the rendered block output. + * + * @param string $content The post content. + * + * @return string The rendered HTML. + */ + private function render_post_with_content( string $content ): string { + update_option( Settings::FIXER_OPTION, Settings::FIXER_OPTION_REPLACE_LINK ); + + $post_id = self::factory()->post->create(); + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => $content, + 'post_type' => 'post', + ) + ); + + $GLOBALS['post'] = get_post( $post_id ); + + return do_blocks( $GLOBALS['post']->post_content ); + } + + /** + * @testdox The render_block output should use a span with the data-iawmlf-links attribute, not a script tag. + * + * @return void + */ + public function test_render_block_output_is_span_with_data_attribute(): void { + $rendered = $this->render_post_with_content( 'Hi a' ); + + $this->assertStringContainsString( 'assertStringContainsString( 'class="__iawmlf-post-loop-links"', $rendered ); + $this->assertStringContainsString( 'data-iawmlf-links="', $rendered ); + $this->assertStringNotContainsString( '