From 8b3e543172e3b796b098fa001ddabb807a712a82 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Mon, 16 Mar 2026 18:01:36 -0500 Subject: [PATCH 1/3] Add popup-blocked fallback for bookmarklet on mobile browsers When window.open() fails (common on mobile browsers like Firefox Mobile), fall back to navigating the current tab with scraped data encoded as JSON in a _data URL parameter. The PHP backend parses and sanitizes this inline data through the same limit_* methods used for POST data, and the React app skips the postMessage listener when inline data mode is active. Arrays are trimmed (5 images, 3 embeds) in the fallback path to keep URL length manageable. See WordPress/press-this#50 --- assets/bookmarklet.js | 17 +++- assets/bookmarklet.min.js | 2 +- class-wp-press-this-plugin.php | 112 +++++++++++++++++++++++++- src/App.js | 8 +- tests/bookmarklet/bookmarklet.test.js | 25 ++++++ tests/php/test-integration.php | 74 +++++++++++++++++ 6 files changed, 233 insertions(+), 5 deletions(-) diff --git a/assets/bookmarklet.js b/assets/bookmarklet.js index 234cda1..938e6d9 100644 --- a/assets/bookmarklet.js +++ b/assets/bookmarklet.js @@ -340,6 +340,19 @@ // Open popup window directly (GET request sends session cookies). popup = window.open( pt_url, target, 'location,resizable,scrollbars,width=' + windowWidth + ',height=' + windowHeight ); - // Send scraped data via postMessage. - sendDataToPopup(); + if ( popup ) { + // Send scraped data via postMessage. + sendDataToPopup(); + } else { + // Popup blocked (common on mobile browsers). Navigate current window with inline data. + // Trim arrays to limit URL length for the fallback path. + if ( scrapedData._images && scrapedData._images.length > 5 ) { + scrapedData._images = scrapedData._images.slice( 0, 5 ); + } + if ( scrapedData._embeds && scrapedData._embeds.length > 3 ) { + scrapedData._embeds = scrapedData._embeds.slice( 0, 3 ); + } + + top.location.href = pt_url + '&_data=' + encURI( JSON.stringify( scrapedData ) ); + } } )( window, document, top.location.href, window.pt_url ); diff --git a/assets/bookmarklet.min.js b/assets/bookmarklet.min.js index 00bb55c..5f0e145 100644 --- a/assets/bookmarklet.min.js +++ b/assets/bookmarklet.min.js @@ -1 +1 @@ -!function(e,t,i,a){var n,o,r,l,c,s,g,m,d,f,h,u=e.encodeURIComponent,p=t.getElementsByTagName("head")[0],y={};if(a)if(i.match(/^https?:/)){a+="&u="+u(i),e.getSelection?r=e.getSelection()+"":t.getSelection?r=t.getSelection()+"":t.selection&&(r=t.selection.createRange().text||""),a+="&buster="+(new Date).getTime(),a+="&pm=1",n=(n=e.outerWidth||t.documentElement.clientWidth||600)<800||n>5e3?600:.7*n,o=(o=e.outerHeight||t.documentElement.clientHeight||700)<800||o>3e3?700:.9*o,T("pt_version",11),l=p.getElementsByTagName("meta")||[];for(var v=0;v200);v++){var b=l[v],O=b.getAttribute("name"),_=b.getAttribute("property"),x=b.getAttribute("content");x&&(O?T("_meta["+O+"]",x):_&&(T("_meta["+_+"]",x),"og:video"!==_&&"og:video:url"!==_&&"og:video:secure_url"!==_||T("_og_video[]",x)))}c=p.getElementsByTagName("link")||[];for(var E=0;E=50);E++){var A=c[E],w=A.getAttribute("rel");"canonical"!==w&&"icon"!==w&&"shortlink"!==w||T("_links["+w+"]",A.getAttribute("href")),"alternate"===w&&"x-default"===A.getAttribute("hreflang")&&T("_links[alternate_canonical]",A.getAttribute("href"))}!function(){f=t.querySelectorAll('script[type="application/ld+json"]');for(var e=0;e=100);N++)(d=g[N]).src.indexOf("avatar")>-1||d.className.indexOf("avatar")>-1||d.width&&d.width<256||d.height&&d.height<128||d.src&&0!==d.src.indexOf("data:")&&T("_images[]",d.src);m=t.body.getElementsByTagName("iframe")||[];for(var j=0;j=50);j++){var B=m[j].src;B&&"about:blank"!==B&&(B.indexOf("jetpack-comment")>-1||B.indexOf("disqus.com")>-1||B.indexOf("facebook.com/plugins")>-1||B.indexOf("platform.twitter.com/widgets")>-1||B.indexOf("google.com/recaptcha")>-1||B.indexOf("googletagmanager.com")>-1||B.indexOf("doubleclick.net")>-1||B.indexOf("googlesyndication.com")>-1||B.indexOf("amazon-adsystem.com")>-1||B.indexOf("quantserve.com")>-1||B.indexOf("scorecardresearch.com")>-1||B.indexOf("addthis.com")>-1||B.indexOf("sharethis.com")>-1||B.indexOf("addtoany.com")>-1||T("_embeds[]",B))}var k,P;t.title&&T("t",t.title),r&&T("s",r),h=e.open(a,"_press_this_app","location,resizable,scrollbars,width="+n+",height="+o),k=0,P=a.match(/^https?:\/\/[^\/]+/)[0],setTimeout(function e(){if(k++,h&&!h.closed){try{h.postMessage({type:"press-this-data",version:11,data:y},P)}catch(e){}k<50&&setTimeout(e,100)}},200)}else top.location.href=a;function T(e,t){if(null!=t&&""!==t){var i=e.match(/^(.+)\[\]$/);if(i){var a=i[1];return y[a]||(y[a]=[]),void y[a].push(t)}var n=e.match(/^(.+)\[(.+)\]$/);if(n){var o=n[1],r=n[2];return y[o]||(y[o]={}),void(y[o][r]=t)}y[e]=t}}function S(e){if(e&&"object"==typeof e){var t=e["@type"];if("VideoObject"===t&&(e.embedUrl&&T("_embeds[]",e.embedUrl),e.contentUrl&&!e.embedUrl&&T("_embeds[]",e.contentUrl)),"Article"!==t&&"WebPage"!==t&&"NewsArticle"!==t&&"BlogPosting"!==t||(e.mainEntityOfPage&&"string"==typeof e.mainEntityOfPage?T("_jsonld[canonical]",e.mainEntityOfPage):e.mainEntityOfPage&&e.mainEntityOfPage["@id"]&&T("_jsonld[canonical]",e.mainEntityOfPage["@id"]),e.headline&&T("_jsonld[headline]",e.headline),e.description&&T("_jsonld[description]",e.description)),e.image){var i="";"string"==typeof e.image?i=e.image:e.image.url?i=e.image.url:Array.isArray(e.image)&&e.image[0]&&(i="string"==typeof e.image[0]?e.image[0]:e.image[0].url),i&&T("_jsonld[image]",i)}}}}(window,document,top.location.href,window.pt_url); \ No newline at end of file +!function(e,t,i,a){var n,o,r,s,l,c,g,m,d,f,h,p=e.encodeURIComponent,u=t.getElementsByTagName("head")[0],y={};if(a)if(i.match(/^https?:/)){a+="&u="+p(i),e.getSelection?r=e.getSelection()+"":t.getSelection?r=t.getSelection()+"":t.selection&&(r=t.selection.createRange().text||""),a+="&buster="+(new Date).getTime(),a+="&pm=1",n=(n=e.outerWidth||t.documentElement.clientWidth||600)<800||n>5e3?600:.7*n,o=(o=e.outerHeight||t.documentElement.clientHeight||700)<800||o>3e3?700:.9*o,T("pt_version",11),s=u.getElementsByTagName("meta")||[];for(var b=0;b200);b++){var _=s[b],v=_.getAttribute("name"),O=_.getAttribute("property"),x=_.getAttribute("content");x&&(v?T("_meta["+v+"]",x):O&&(T("_meta["+O+"]",x),"og:video"!==O&&"og:video:url"!==O&&"og:video:secure_url"!==O||T("_og_video[]",x)))}l=u.getElementsByTagName("link")||[];for(var E=0;E=50);E++){var A=l[E],w=A.getAttribute("rel");"canonical"!==w&&"icon"!==w&&"shortlink"!==w||T("_links["+w+"]",A.getAttribute("href")),"alternate"===w&&"x-default"===A.getAttribute("hreflang")&&T("_links[alternate_canonical]",A.getAttribute("href"))}!function(){f=t.querySelectorAll('script[type="application/ld+json"]');for(var e=0;e=100);N++)(d=g[N]).src.indexOf("avatar")>-1||d.className.indexOf("avatar")>-1||d.width&&d.width<256||d.height&&d.height<128||d.src&&0!==d.src.indexOf("data:")&&T("_images[]",d.src);m=t.body.getElementsByTagName("iframe")||[];for(var j=0;j=50);j++){var B=m[j].src;B&&"about:blank"!==B&&(B.indexOf("jetpack-comment")>-1||B.indexOf("disqus.com")>-1||B.indexOf("facebook.com/plugins")>-1||B.indexOf("platform.twitter.com/widgets")>-1||B.indexOf("google.com/recaptcha")>-1||B.indexOf("googletagmanager.com")>-1||B.indexOf("doubleclick.net")>-1||B.indexOf("googlesyndication.com")>-1||B.indexOf("amazon-adsystem.com")>-1||B.indexOf("quantserve.com")>-1||B.indexOf("scorecardresearch.com")>-1||B.indexOf("addthis.com")>-1||B.indexOf("sharethis.com")>-1||B.indexOf("addtoany.com")>-1||T("_embeds[]",B))}var k,P;t.title&&T("t",t.title),r&&T("s",r),(h=e.open(a,"_press_this_app","location,resizable,scrollbars,width="+n+",height="+o))?(k=0,P=a.match(/^https?:\/\/[^\/]+/)[0],setTimeout(function e(){if(k++,h&&!h.closed){try{h.postMessage({type:"press-this-data",version:11,data:y},P)}catch(e){}k<50&&setTimeout(e,100)}},200)):(y._images&&y._images.length>5&&(y._images=y._images.slice(0,5)),y._embeds&&y._embeds.length>3&&(y._embeds=y._embeds.slice(0,3)),top.location.href=a+"&_data="+p(JSON.stringify(y)))}else top.location.href=a;function T(e,t){if(null!=t&&""!==t){var i=e.match(/^(.+)\[\]$/);if(i){var a=i[1];return y[a]||(y[a]=[]),void y[a].push(t)}var n=e.match(/^(.+)\[(.+)\]$/);if(n){var o=n[1],r=n[2];return y[o]||(y[o]={}),void(y[o][r]=t)}y[e]=t}}function S(e){if(e&&"object"==typeof e){var t=e["@type"];if("VideoObject"===t&&(e.embedUrl&&T("_embeds[]",e.embedUrl),e.contentUrl&&!e.embedUrl&&T("_embeds[]",e.contentUrl)),"Article"!==t&&"WebPage"!==t&&"NewsArticle"!==t&&"BlogPosting"!==t||(e.mainEntityOfPage&&"string"==typeof e.mainEntityOfPage?T("_jsonld[canonical]",e.mainEntityOfPage):e.mainEntityOfPage&&e.mainEntityOfPage["@id"]&&T("_jsonld[canonical]",e.mainEntityOfPage["@id"]),e.headline&&T("_jsonld[headline]",e.headline),e.description&&T("_jsonld[description]",e.description)),e.image){var i="";"string"==typeof e.image?i=e.image:e.image.url?i=e.image.url:Array.isArray(e.image)&&e.image[0]&&(i="string"==typeof e.image[0]?e.image[0]:e.image[0].url),i&&T("_jsonld[image]",i)}}}}(window,document,top.location.href,window.pt_url); \ No newline at end of file diff --git a/class-wp-press-this-plugin.php b/class-wp-press-this-plugin.php index f446d35..463481b 100644 --- a/class-wp-press-this-plugin.php +++ b/class-wp-press-this-plugin.php @@ -49,6 +49,13 @@ class WP_Press_This_Plugin { */ private $domain = ''; + /** + * Whether data was received inline via URL (popup-blocked fallback). + * + * @var bool + */ + private $inline_data_mode = false; + /** * Constructor. * @@ -768,6 +775,105 @@ public function merge_or_fetch_data() { } } + // Support inline data from bookmarklet fallback (popup blocked on mobile). + // When window.open() fails, the bookmarklet encodes scraped data as JSON in the URL. + if ( ! empty( $_GET['_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $inline_data = json_decode( wp_unslash( $_GET['_data'] ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( is_array( $inline_data ) ) { + // Process simple string fields. + $field_map = array( + 'u' => 'u', + 't' => 't', + 's' => 's', + 'pt_version' => 'v', + ); + foreach ( $field_map as $inline_key => $data_key ) { + if ( ! empty( $inline_data[ $inline_key ] ) && empty( $data[ $data_key ] ) ) { + if ( 'u' === $data_key ) { + $data[ $data_key ] = $this->limit_url( $inline_data[ $inline_key ] ); + } else { + $data[ $data_key ] = $this->limit_string( $inline_data[ $inline_key ] ); + } + } + } + + // Process media arrays. + foreach ( array( '_images', '_embeds', '_og_video' ) as $type ) { + if ( empty( $inline_data[ $type ] ) || ! is_array( $inline_data[ $type ] ) ) { + continue; + } + + if ( ! isset( $data[ $type ] ) ) { + $data[ $type ] = array(); + } + + $items = $this->limit_array( $inline_data[ $type ] ); + + foreach ( $items as $value ) { + if ( '_images' === $type ) { + $value = $this->limit_img( $value ); + } else { + $value = $this->limit_embed( $value ); + } + + if ( ! empty( $value ) && ! in_array( $value, $data[ $type ], true ) ) { + if ( '_og_video' === $type ) { + if ( ! isset( $data['_embeds'] ) ) { + $data['_embeds'] = array(); + } + if ( ! in_array( $value, $data['_embeds'], true ) ) { + $data['_embeds'][] = $value; + } + } else { + $data[ $type ][] = $value; + } + } + } + } + + // Process metadata objects. + foreach ( array( '_meta', '_links', '_jsonld' ) as $type ) { + if ( empty( $inline_data[ $type ] ) || ! is_array( $inline_data[ $type ] ) ) { + continue; + } + + if ( ! isset( $data[ $type ] ) ) { + $data[ $type ] = array(); + } + + $items = $this->limit_array( $inline_data[ $type ] ); + + foreach ( $items as $key => $value ) { + if ( empty( $key ) || strlen( $key ) > 100 ) { + continue; + } + + if ( '_meta' === $type ) { + $value = $this->limit_string( $value ); + if ( ! empty( $value ) ) { + $data = $this->process_meta_entry( $key, $value, $data ); + } + } elseif ( '_links' === $type ) { + if ( in_array( $key, array( 'canonical', 'shortlink', 'icon', 'alternate_canonical' ), true ) ) { + $data[ $type ][ $key ] = $this->limit_url( $value ); + } + } elseif ( '_jsonld' === $type ) { + if ( in_array( $key, array( 'canonical', 'headline', 'description', 'image' ), true ) ) { + if ( 'canonical' === $key || 'image' === $key ) { + $data[ $type ][ $key ] = $this->limit_url( $value ); + } else { + $data[ $type ][ $key ] = $this->limit_string( $value ); + } + } + } + } + } + + $this->inline_data_mode = true; + } + } + // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended /** @@ -1467,7 +1573,7 @@ public function html() { $proxy_enabled = press_this_is_proxy_enabled(); $has_get_selection = ! empty( $data['s'] ) && 'GET' === $_SERVER['REQUEST_METHOD']; - if ( $is_post_request || ! $proxy_enabled || $has_get_selection ) { + if ( $is_post_request || $this->inline_data_mode || ! $proxy_enabled || $has_get_selection ) { $post_title = $this->get_suggested_title( $data ); $post_content = $this->get_suggested_content( $data ); } else { @@ -1623,6 +1729,10 @@ public function html() { // PostMessage mode (bookmarklet v11+). // When true, the app waits for scraped data via postMessage from the opener. 'postMessageMode' => $is_post_message_mode, + + // Inline data mode (popup-blocked fallback). + // When true, scraped data was passed via URL and already merged into $data above. + 'inlineDataMode' => $this->inline_data_mode, ); if ( ! headers_sent() ) { diff --git a/src/App.js b/src/App.js index 32ba56f..868db3c 100644 --- a/src/App.js +++ b/src/App.js @@ -182,7 +182,12 @@ export default function App() { */ useEffect( () => { // Only listen if we're in postMessage mode and haven't received data yet. - if ( ! data.postMessageMode || postMessageReceived ) { + // Skip if inline data mode — data was passed via URL, not postMessage. + if ( + ! data.postMessageMode || + data.inlineDataMode || + postMessageReceived + ) { return; } @@ -267,6 +272,7 @@ export default function App() { }; }, [ data.postMessageMode, + data.inlineDataMode, data.restUrl, data.restNonce, data.sourceUrl, diff --git a/tests/bookmarklet/bookmarklet.test.js b/tests/bookmarklet/bookmarklet.test.js index 2e0263e..6c4677f 100644 --- a/tests/bookmarklet/bookmarklet.test.js +++ b/tests/bookmarklet/bookmarklet.test.js @@ -218,6 +218,31 @@ describe( 'Bookmarklet Functionality', () => { ).toBe( true ); } ); + test( 'Falls back to current-window navigation when popup is blocked', () => { + // Check for popup null check (fallback for mobile browsers). + expect( bookmarkletSource ).toContain( 'if ( popup )' ); + + // Check for inline data encoding via JSON.stringify. + expect( bookmarkletSource ).toContain( 'JSON.stringify' ); + + // Check for _data URL parameter in fallback path. + expect( bookmarkletSource ).toContain( '_data=' ); + + // Check for top.location.href fallback navigation. + expect( bookmarkletSource ).toContain( 'top.location.href' ); + } ); + + test( 'Trims arrays in popup-blocked fallback to limit URL length', () => { + // Check for image array trimming. + expect( bookmarkletSource ).toContain( 'scrapedData._images.length > 5' ); + + // Check for embed array trimming. + expect( bookmarkletSource ).toContain( 'scrapedData._embeds.length > 3' ); + + // Check for slice usage. + expect( bookmarkletSource ).toContain( '.slice( 0,' ); + } ); + test( 'Enhanced data extraction - JSON-LD structured data', () => { // Check for JSON-LD script tag query. expect( bookmarkletSource ).toContain( 'application/ld+json' ); diff --git a/tests/php/test-integration.php b/tests/php/test-integration.php index 7e7db75..3ba1efc 100644 --- a/tests/php/test-integration.php +++ b/tests/php/test-integration.php @@ -283,4 +283,78 @@ public function test_rest_save_with_external_images_succeeds() { remove_filter( 'pre_http_request', $block_http, 1 ); } + + /** + * Test: Inline data parameter (_data) is parsed by merge_or_fetch_data. + * + * When the bookmarklet's popup is blocked (mobile browsers), it falls back + * to passing scraped data as JSON in the _data URL parameter. + */ + public function test_inline_data_parameter_is_parsed() { + $scraped_data = array( + 'u' => 'https://example.com/article', + 't' => 'Example Article', + 's' => 'Selected text', + '_images' => array( 'https://example.com/image.jpg' ), + '_embeds' => array( 'https://www.youtube.com/embed/abc?si=123' ), + '_meta' => array( + 'og:title' => 'OG Title', + 'og:description' => 'OG Description', + ), + '_links' => array( + 'canonical' => 'https://example.com/article', + ), + ); + + $_GET['_data'] = wp_json_encode( $scraped_data ); + + $data = $this->plugin->merge_or_fetch_data(); + + $this->assertEquals( 'https://example.com/article', $data['u'] ); + $this->assertEquals( 'Example Article', $data['t'] ); + $this->assertEquals( 'Selected text', $data['s'] ); + $this->assertContains( 'https://example.com/image.jpg', $data['_images'] ); + // limit_embed() transforms YouTube embed URLs to watch URLs. + $this->assertContains( 'https://www.youtube.com/watch?v=abc', $data['_embeds'] ); + $this->assertEquals( 'https://example.com/article', $data['_links']['canonical'] ); + + unset( $_GET['_data'] ); + } + + /** + * Test: Inline data does not override existing GET/POST parameters. + */ + public function test_inline_data_does_not_override_existing_params() { + $_GET['u'] = 'https://existing.com/page'; + + $scraped_data = array( + 'u' => 'https://example.com/different', + 't' => 'Inline Title', + ); + + $_GET['_data'] = wp_json_encode( $scraped_data ); + + $data = $this->plugin->merge_or_fetch_data(); + + // Existing 'u' param should take precedence. + $this->assertEquals( 'https://existing.com/page', $data['u'] ); + // 't' should come from inline data since it wasn't in GET. + $this->assertEquals( 'Inline Title', $data['t'] ); + + unset( $_GET['_data'], $_GET['u'] ); + } + + /** + * Test: Invalid JSON in _data parameter is handled gracefully. + */ + public function test_invalid_inline_data_is_handled_gracefully() { + $_GET['_data'] = 'not valid json{{{'; + + $data = $this->plugin->merge_or_fetch_data(); + + // Should not crash, should return normal empty-ish data. + $this->assertIsArray( $data ); + + unset( $_GET['_data'] ); + } } From 7d3717d0f1cd44b4172113c7f308890abbc02723 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Mon, 16 Mar 2026 18:55:14 -0500 Subject: [PATCH 2/3] Harden popup-blocked fallback per code review feedback - Respect enable_press_this_media_discovery filter for inline data path - Build minimal fallback payload (only used meta keys, canonical/shortlink) with 7500-char URL cap to avoid browser truncation - Override pm=0 in fallback URL so app doesn't wait for postMessage - Strip _data param from browser history via replaceState Fixes #50 --- assets/bookmarklet.js | 53 +++++++++++-- assets/bookmarklet.min.js | 2 +- class-wp-press-this-plugin.php | 108 +++++++++++++------------- src/App.js | 9 +++ tests/bookmarklet/bookmarklet.test.js | 18 +++-- 5 files changed, 124 insertions(+), 66 deletions(-) diff --git a/assets/bookmarklet.js b/assets/bookmarklet.js index 938e6d9..e409a97 100644 --- a/assets/bookmarklet.js +++ b/assets/bookmarklet.js @@ -345,14 +345,55 @@ sendDataToPopup(); } else { // Popup blocked (common on mobile browsers). Navigate current window with inline data. - // Trim arrays to limit URL length for the fallback path. - if ( scrapedData._images && scrapedData._images.length > 5 ) { - scrapedData._images = scrapedData._images.slice( 0, 5 ); + // Build a minimal payload to stay within URL length limits (~8KB in modern browsers). + var fallbackData = { + t: scrapedData.t, + s: scrapedData.s, + pt_version: scrapedData.pt_version + }; + + if ( scrapedData._images ) { + fallbackData._images = scrapedData._images.slice( 0, 5 ); } - if ( scrapedData._embeds && scrapedData._embeds.length > 3 ) { - scrapedData._embeds = scrapedData._embeds.slice( 0, 3 ); + if ( scrapedData._embeds ) { + fallbackData._embeds = scrapedData._embeds.slice( 0, 3 ); } - top.location.href = pt_url + '&_data=' + encURI( JSON.stringify( scrapedData ) ); + // Only include the specific meta keys Press This uses. + if ( scrapedData._meta ) { + var usedMetaKeys = [ 'og:title', 'og:description', 'og:site_name', 'og:video', 'og:video:url', 'og:video:secure_url', 'twitter:title', 'twitter:description', 'description', 'title' ]; + fallbackData._meta = {}; + for ( var mk = 0; mk < usedMetaKeys.length; mk++ ) { + if ( scrapedData._meta[ usedMetaKeys[ mk ] ] ) { + fallbackData._meta[ usedMetaKeys[ mk ] ] = scrapedData._meta[ usedMetaKeys[ mk ] ]; + } + } + } + + // Include only canonical and shortlink from links. + if ( scrapedData._links ) { + fallbackData._links = {}; + if ( scrapedData._links.canonical ) { + fallbackData._links.canonical = scrapedData._links.canonical; + } + if ( scrapedData._links.shortlink ) { + fallbackData._links.shortlink = scrapedData._links.shortlink; + } + } + + // Skip _jsonld entirely — low value for the URL budget. + + // Override pm flag so the app doesn't wait for postMessage. + var fallbackUrl = pt_url.replace( '&pm=1', '&pm=0' ) + '&_data=' + encURI( JSON.stringify( fallbackData ) ); + + // Enforce a max URL length to avoid truncation. + if ( fallbackUrl.length > 7500 ) { + // Strip meta/links and try again with just title, selection, and media. + delete fallbackData._meta; + delete fallbackData._links; + fallbackUrl = pt_url.replace( '&pm=1', '&pm=0' ) + '&_data=' + encURI( JSON.stringify( fallbackData ) ); + } + + top.location.href = fallbackUrl; } } )( window, document, top.location.href, window.pt_url ); diff --git a/assets/bookmarklet.min.js b/assets/bookmarklet.min.js index 5f0e145..005686b 100644 --- a/assets/bookmarklet.min.js +++ b/assets/bookmarklet.min.js @@ -1 +1 @@ -!function(e,t,i,a){var n,o,r,s,l,c,g,m,d,f,h,p=e.encodeURIComponent,u=t.getElementsByTagName("head")[0],y={};if(a)if(i.match(/^https?:/)){a+="&u="+p(i),e.getSelection?r=e.getSelection()+"":t.getSelection?r=t.getSelection()+"":t.selection&&(r=t.selection.createRange().text||""),a+="&buster="+(new Date).getTime(),a+="&pm=1",n=(n=e.outerWidth||t.documentElement.clientWidth||600)<800||n>5e3?600:.7*n,o=(o=e.outerHeight||t.documentElement.clientHeight||700)<800||o>3e3?700:.9*o,T("pt_version",11),s=u.getElementsByTagName("meta")||[];for(var b=0;b200);b++){var _=s[b],v=_.getAttribute("name"),O=_.getAttribute("property"),x=_.getAttribute("content");x&&(v?T("_meta["+v+"]",x):O&&(T("_meta["+O+"]",x),"og:video"!==O&&"og:video:url"!==O&&"og:video:secure_url"!==O||T("_og_video[]",x)))}l=u.getElementsByTagName("link")||[];for(var E=0;E=50);E++){var A=l[E],w=A.getAttribute("rel");"canonical"!==w&&"icon"!==w&&"shortlink"!==w||T("_links["+w+"]",A.getAttribute("href")),"alternate"===w&&"x-default"===A.getAttribute("hreflang")&&T("_links[alternate_canonical]",A.getAttribute("href"))}!function(){f=t.querySelectorAll('script[type="application/ld+json"]');for(var e=0;e=100);N++)(d=g[N]).src.indexOf("avatar")>-1||d.className.indexOf("avatar")>-1||d.width&&d.width<256||d.height&&d.height<128||d.src&&0!==d.src.indexOf("data:")&&T("_images[]",d.src);m=t.body.getElementsByTagName("iframe")||[];for(var j=0;j=50);j++){var B=m[j].src;B&&"about:blank"!==B&&(B.indexOf("jetpack-comment")>-1||B.indexOf("disqus.com")>-1||B.indexOf("facebook.com/plugins")>-1||B.indexOf("platform.twitter.com/widgets")>-1||B.indexOf("google.com/recaptcha")>-1||B.indexOf("googletagmanager.com")>-1||B.indexOf("doubleclick.net")>-1||B.indexOf("googlesyndication.com")>-1||B.indexOf("amazon-adsystem.com")>-1||B.indexOf("quantserve.com")>-1||B.indexOf("scorecardresearch.com")>-1||B.indexOf("addthis.com")>-1||B.indexOf("sharethis.com")>-1||B.indexOf("addtoany.com")>-1||T("_embeds[]",B))}var k,P;t.title&&T("t",t.title),r&&T("s",r),(h=e.open(a,"_press_this_app","location,resizable,scrollbars,width="+n+",height="+o))?(k=0,P=a.match(/^https?:\/\/[^\/]+/)[0],setTimeout(function e(){if(k++,h&&!h.closed){try{h.postMessage({type:"press-this-data",version:11,data:y},P)}catch(e){}k<50&&setTimeout(e,100)}},200)):(y._images&&y._images.length>5&&(y._images=y._images.slice(0,5)),y._embeds&&y._embeds.length>3&&(y._embeds=y._embeds.slice(0,3)),top.location.href=a+"&_data="+p(JSON.stringify(y)))}else top.location.href=a;function T(e,t){if(null!=t&&""!==t){var i=e.match(/^(.+)\[\]$/);if(i){var a=i[1];return y[a]||(y[a]=[]),void y[a].push(t)}var n=e.match(/^(.+)\[(.+)\]$/);if(n){var o=n[1],r=n[2];return y[o]||(y[o]={}),void(y[o][r]=t)}y[e]=t}}function S(e){if(e&&"object"==typeof e){var t=e["@type"];if("VideoObject"===t&&(e.embedUrl&&T("_embeds[]",e.embedUrl),e.contentUrl&&!e.embedUrl&&T("_embeds[]",e.contentUrl)),"Article"!==t&&"WebPage"!==t&&"NewsArticle"!==t&&"BlogPosting"!==t||(e.mainEntityOfPage&&"string"==typeof e.mainEntityOfPage?T("_jsonld[canonical]",e.mainEntityOfPage):e.mainEntityOfPage&&e.mainEntityOfPage["@id"]&&T("_jsonld[canonical]",e.mainEntityOfPage["@id"]),e.headline&&T("_jsonld[headline]",e.headline),e.description&&T("_jsonld[description]",e.description)),e.image){var i="";"string"==typeof e.image?i=e.image:e.image.url?i=e.image.url:Array.isArray(e.image)&&e.image[0]&&(i="string"==typeof e.image[0]?e.image[0]:e.image[0].url),i&&T("_jsonld[image]",i)}}}}(window,document,top.location.href,window.pt_url); \ No newline at end of file +!function(e,t,i,a){var n,o,r,l,s,c,m,g,d,f,h,p=e.encodeURIComponent,_=t.getElementsByTagName("head")[0],u={};if(a)if(i.match(/^https?:/)){a+="&u="+p(i),e.getSelection?r=e.getSelection()+"":t.getSelection?r=t.getSelection()+"":t.selection&&(r=t.selection.createRange().text||""),a+="&buster="+(new Date).getTime(),a+="&pm=1",n=(n=e.outerWidth||t.documentElement.clientWidth||600)<800||n>5e3?600:.7*n,o=(o=e.outerHeight||t.documentElement.clientHeight||700)<800||o>3e3?700:.9*o,q("pt_version",11),l=_.getElementsByTagName("meta")||[];for(var y=0;y200);y++){var v=l[y],b=v.getAttribute("name"),O=v.getAttribute("property"),k=v.getAttribute("content");k&&(b?q("_meta["+b+"]",k):O&&(q("_meta["+O+"]",k),"og:video"!==O&&"og:video:url"!==O&&"og:video:secure_url"!==O||q("_og_video[]",k)))}s=_.getElementsByTagName("link")||[];for(var x=0;x=50);x++){var E=s[x],A=E.getAttribute("rel");"canonical"!==A&&"icon"!==A&&"shortlink"!==A||q("_links["+A+"]",E.getAttribute("href")),"alternate"===A&&"x-default"===E.getAttribute("hreflang")&&q("_links[alternate_canonical]",E.getAttribute("href"))}!function(){f=t.querySelectorAll('script[type="application/ld+json"]');for(var e=0;e=100);w++)(d=m[w]).src.indexOf("avatar")>-1||d.className.indexOf("avatar")>-1||d.width&&d.width<256||d.height&&d.height<128||d.src&&0!==d.src.indexOf("data:")&&q("_images[]",d.src);g=t.body.getElementsByTagName("iframe")||[];for(var N=0;N=50);N++){var j=g[N].src;j&&"about:blank"!==j&&(j.indexOf("jetpack-comment")>-1||j.indexOf("disqus.com")>-1||j.indexOf("facebook.com/plugins")>-1||j.indexOf("platform.twitter.com/widgets")>-1||j.indexOf("google.com/recaptcha")>-1||j.indexOf("googletagmanager.com")>-1||j.indexOf("doubleclick.net")>-1||j.indexOf("googlesyndication.com")>-1||j.indexOf("amazon-adsystem.com")>-1||j.indexOf("quantserve.com")>-1||j.indexOf("scorecardresearch.com")>-1||j.indexOf("addthis.com")>-1||j.indexOf("sharethis.com")>-1||j.indexOf("addtoany.com")>-1||q("_embeds[]",j))}if(t.title&&q("t",t.title),r&&q("s",r),h=e.open(a,"_press_this_app","location,resizable,scrollbars,width="+n+",height="+o))U=0,C=a.match(/^https?:\/\/[^\/]+/)[0],setTimeout(function e(){if(U++,h&&!h.closed){try{h.postMessage({type:"press-this-data",version:11,data:u},C)}catch(e){}U<50&&setTimeout(e,100)}},200);else{var B={t:u.t,s:u.s,pt_version:u.pt_version};if(u._images&&(B._images=u._images.slice(0,5)),u._embeds&&(B._embeds=u._embeds.slice(0,3)),u._meta){var P=["og:title","og:description","og:site_name","og:video","og:video:url","og:video:secure_url","twitter:title","twitter:description","description","title"];B._meta={};for(var S=0;S7500&&(delete B._meta,delete B._links,T=a.replace("&pm=1","&pm=0")+"&_data="+p(JSON.stringify(B))),top.location.href=T}var U,C}else top.location.href=a;function q(e,t){if(null!=t&&""!==t){var i=e.match(/^(.+)\[\]$/);if(i){var a=i[1];return u[a]||(u[a]=[]),void u[a].push(t)}var n=e.match(/^(.+)\[(.+)\]$/);if(n){var o=n[1],r=n[2];return u[o]||(u[o]={}),void(u[o][r]=t)}u[e]=t}}function J(e){if(e&&"object"==typeof e){var t=e["@type"];if("VideoObject"===t&&(e.embedUrl&&q("_embeds[]",e.embedUrl),e.contentUrl&&!e.embedUrl&&q("_embeds[]",e.contentUrl)),"Article"!==t&&"WebPage"!==t&&"NewsArticle"!==t&&"BlogPosting"!==t||(e.mainEntityOfPage&&"string"==typeof e.mainEntityOfPage?q("_jsonld[canonical]",e.mainEntityOfPage):e.mainEntityOfPage&&e.mainEntityOfPage["@id"]&&q("_jsonld[canonical]",e.mainEntityOfPage["@id"]),e.headline&&q("_jsonld[headline]",e.headline),e.description&&q("_jsonld[description]",e.description)),e.image){var i="";"string"==typeof e.image?i=e.image:e.image.url?i=e.image.url:Array.isArray(e.image)&&e.image[0]&&(i="string"==typeof e.image[0]?e.image[0]:e.image[0].url),i&&q("_jsonld[image]",i)}}}}(window,document,top.location.href,window.pt_url); \ No newline at end of file diff --git a/class-wp-press-this-plugin.php b/class-wp-press-this-plugin.php index 463481b..3ba1cd8 100644 --- a/class-wp-press-this-plugin.php +++ b/class-wp-press-this-plugin.php @@ -798,72 +798,76 @@ public function merge_or_fetch_data() { } } - // Process media arrays. - foreach ( array( '_images', '_embeds', '_og_video' ) as $type ) { - if ( empty( $inline_data[ $type ] ) || ! is_array( $inline_data[ $type ] ) ) { - continue; - } + // Process media and metadata only if media discovery is enabled. + /** This filter is documented above in the POST data section. */ + if ( apply_filters( 'enable_press_this_media_discovery', true ) ) { + // Process media arrays. + foreach ( array( '_images', '_embeds', '_og_video' ) as $type ) { + if ( empty( $inline_data[ $type ] ) || ! is_array( $inline_data[ $type ] ) ) { + continue; + } - if ( ! isset( $data[ $type ] ) ) { - $data[ $type ] = array(); - } + if ( ! isset( $data[ $type ] ) ) { + $data[ $type ] = array(); + } - $items = $this->limit_array( $inline_data[ $type ] ); + $items = $this->limit_array( $inline_data[ $type ] ); - foreach ( $items as $value ) { - if ( '_images' === $type ) { - $value = $this->limit_img( $value ); - } else { - $value = $this->limit_embed( $value ); - } + foreach ( $items as $value ) { + if ( '_images' === $type ) { + $value = $this->limit_img( $value ); + } else { + $value = $this->limit_embed( $value ); + } - if ( ! empty( $value ) && ! in_array( $value, $data[ $type ], true ) ) { - if ( '_og_video' === $type ) { - if ( ! isset( $data['_embeds'] ) ) { - $data['_embeds'] = array(); - } - if ( ! in_array( $value, $data['_embeds'], true ) ) { - $data['_embeds'][] = $value; + if ( ! empty( $value ) && ! in_array( $value, $data[ $type ], true ) ) { + if ( '_og_video' === $type ) { + if ( ! isset( $data['_embeds'] ) ) { + $data['_embeds'] = array(); + } + if ( ! in_array( $value, $data['_embeds'], true ) ) { + $data['_embeds'][] = $value; + } + } else { + $data[ $type ][] = $value; } - } else { - $data[ $type ][] = $value; } } } - } - - // Process metadata objects. - foreach ( array( '_meta', '_links', '_jsonld' ) as $type ) { - if ( empty( $inline_data[ $type ] ) || ! is_array( $inline_data[ $type ] ) ) { - continue; - } - if ( ! isset( $data[ $type ] ) ) { - $data[ $type ] = array(); - } - - $items = $this->limit_array( $inline_data[ $type ] ); - - foreach ( $items as $key => $value ) { - if ( empty( $key ) || strlen( $key ) > 100 ) { + // Process metadata objects. + foreach ( array( '_meta', '_links', '_jsonld' ) as $type ) { + if ( empty( $inline_data[ $type ] ) || ! is_array( $inline_data[ $type ] ) ) { continue; } - if ( '_meta' === $type ) { - $value = $this->limit_string( $value ); - if ( ! empty( $value ) ) { - $data = $this->process_meta_entry( $key, $value, $data ); - } - } elseif ( '_links' === $type ) { - if ( in_array( $key, array( 'canonical', 'shortlink', 'icon', 'alternate_canonical' ), true ) ) { - $data[ $type ][ $key ] = $this->limit_url( $value ); + if ( ! isset( $data[ $type ] ) ) { + $data[ $type ] = array(); + } + + $items = $this->limit_array( $inline_data[ $type ] ); + + foreach ( $items as $key => $value ) { + if ( empty( $key ) || strlen( $key ) > 100 ) { + continue; } - } elseif ( '_jsonld' === $type ) { - if ( in_array( $key, array( 'canonical', 'headline', 'description', 'image' ), true ) ) { - if ( 'canonical' === $key || 'image' === $key ) { + + if ( '_meta' === $type ) { + $value = $this->limit_string( $value ); + if ( ! empty( $value ) ) { + $data = $this->process_meta_entry( $key, $value, $data ); + } + } elseif ( '_links' === $type ) { + if ( in_array( $key, array( 'canonical', 'shortlink', 'icon', 'alternate_canonical' ), true ) ) { $data[ $type ][ $key ] = $this->limit_url( $value ); - } else { - $data[ $type ][ $key ] = $this->limit_string( $value ); + } + } elseif ( '_jsonld' === $type ) { + if ( in_array( $key, array( 'canonical', 'headline', 'description', 'image' ), true ) ) { + if ( 'canonical' === $key || 'image' === $key ) { + $data[ $type ][ $key ] = $this->limit_url( $value ); + } else { + $data[ $type ][ $key ] = $this->limit_string( $value ); + } } } } diff --git a/src/App.js b/src/App.js index 868db3c..e10de1e 100644 --- a/src/App.js +++ b/src/App.js @@ -37,6 +37,15 @@ function getInitialData() { export default function App() { const data = useMemo( () => getInitialData(), [] ); + // Strip _data param from browser history to avoid leaking scraped content. + useEffect( () => { + if ( data.inlineDataMode && window.history.replaceState ) { + const url = new URL( window.location.href ); + url.searchParams.delete( '_data' ); + window.history.replaceState( null, '', url.toString() ); + } + }, [ data.inlineDataMode ] ); + // State for pending scraped content to append. const [ pendingScrape, setPendingScrape ] = useState( null ); diff --git a/tests/bookmarklet/bookmarklet.test.js b/tests/bookmarklet/bookmarklet.test.js index 6c4677f..b18a1ed 100644 --- a/tests/bookmarklet/bookmarklet.test.js +++ b/tests/bookmarklet/bookmarklet.test.js @@ -232,15 +232,19 @@ describe( 'Bookmarklet Functionality', () => { expect( bookmarkletSource ).toContain( 'top.location.href' ); } ); - test( 'Trims arrays in popup-blocked fallback to limit URL length', () => { - // Check for image array trimming. - expect( bookmarkletSource ).toContain( 'scrapedData._images.length > 5' ); + test( 'Builds minimal fallback payload to limit URL length', () => { + // Check that fallback builds a separate minimal payload. + expect( bookmarkletSource ).toContain( 'fallbackData' ); - // Check for embed array trimming. - expect( bookmarkletSource ).toContain( 'scrapedData._embeds.length > 3' ); + // Check for image and embed trimming via slice. + expect( bookmarkletSource ).toContain( '.slice( 0, 5 )' ); + expect( bookmarkletSource ).toContain( '.slice( 0, 3 )' ); - // Check for slice usage. - expect( bookmarkletSource ).toContain( '.slice( 0,' ); + // Check for URL length enforcement. + expect( bookmarkletSource ).toContain( 'fallbackUrl.length > 7500' ); + + // Check that pm flag is overridden in fallback. + expect( bookmarkletSource ).toContain( "replace( '&pm=1', '&pm=0' )" ); } ); test( 'Enhanced data extraction - JSON-LD structured data', () => { From e6d4dc1527eb9b9d23b9cb5acc2bb11b7feefcfe Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Wed, 18 Mar 2026 21:40:25 -0500 Subject: [PATCH 3/3] Update lock file --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 16d2baf..4d8a0c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27186,7 +27186,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "peer": true, "bin": {