From 24e17e1a7cec0654631adff8d0e4026559e976ee Mon Sep 17 00:00:00 2001 From: selul Date: Mon, 31 Aug 2026 15:21:18 +0300 Subject: [PATCH] feat: stamp the generating browser and OS into the shared details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Copy all details" block now ends with a "Details generated from" line (e.g. "Safari 26.6 on iOS 26.6", "Google Chrome 128 on Windows 11"), and the same row is shown on the details card. Detection runs in the customer's browser — client hints first, user-agent parsing as the fallback — so support learns which browser produced the grant, which is often not the device where the reported issue lives. Co-Authored-By: Claude Fable 5 --- inc/js/parrot.js | 160 ++++++++++++++++++++++++++++++++++++++++++++++ pirate-parrot.php | 5 +- 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/inc/js/parrot.js b/inc/js/parrot.js index e464aa9..3a5783d 100644 --- a/inc/js/parrot.js +++ b/inc/js/parrot.js @@ -7,6 +7,8 @@ ); function init() { + injectGeneratedFrom(); + $( document ).on( "click", ".ti-parrot-copy", function( e ){ e.preventDefault(); @@ -96,6 +98,164 @@ ); } + // The environment is read in the browser on purpose: proxies and privacy + // tools rewrite the HTTP User-Agent header, while what support needs is + // the browser the customer actually generated these details from. + function injectGeneratedFrom() { + var card = document.querySelector( ".ti-parrot-details" ); + var button = document.querySelector( ".ti-parrot-copy" ); + if ( ! card || ! button ) { + return; + } + detectEnvironment( + function( env ) { + if ( ! env ) { + return; + } + var label = ( typeof pp !== "undefined" && pp.generatedFrom ) ? pp.generatedFrom : "Details generated from"; + var row = document.createElement( "div" ); + var name = document.createElement( "span" ); + var value = document.createElement( "span" ); + + row.className = "ti-parrot-row"; + name.className = "ti-parrot-row-label"; + value.className = "ti-parrot-row-value"; + + name.textContent = label; + value.textContent = env; + row.appendChild( name ); + row.appendChild( value ); + + var rows = card.querySelectorAll( ".ti-parrot-row" ); + if ( rows.length ) { + var last = rows[ rows.length - 1 ]; + last.parentNode.insertBefore( row, last.nextSibling ); + } else { + card.appendChild( row ); + } + + var text = button.getAttribute( "data-clipboard-text" ) || ""; + button.setAttribute( "data-clipboard-text", text + "\n" + label + ": " + env ); + } + ); + } + + function detectEnvironment( callback ) { + var uad = navigator.userAgentData; + if ( uad && uad.getHighEntropyValues ) { + uad.getHighEntropyValues( [ "platformVersion", "fullVersionList" ] ).then( + function( hints ) { + callback( formatFromClientHints( uad, hints ) || parseUserAgent( navigator.userAgent ) ); + } + ).catch( + function() { + callback( parseUserAgent( navigator.userAgent ) ); + } + ); + return; + } + callback( parseUserAgent( navigator.userAgent ) ); + } + + function formatFromClientHints( uad, hints ) { + var list = ( hints && hints.fullVersionList && hints.fullVersionList.length ) ? hints.fullVersionList : ( uad.brands || [] ); + var brand = null; + for ( var i = 0; i < list.length; i++ ) { + if ( /not.?a.?brand/i.test( list[ i ].brand ) ) { + continue; + } + if ( ! brand || "Chromium" === brand.brand ) { + brand = list[ i ]; + } + } + if ( ! brand ) { + return null; + } + + var browser = brand.brand + " " + String( brand.version ).split( "." )[ 0 ]; + var os = uad.platform || ""; + var version = String( ( hints && hints.platformVersion ) || "" ); + if ( "Windows" === os && version ) { + // Chromium encodes Windows in platformVersion majors: + // 13+ is Windows 11, 1-12 is Windows 10, 0 predates Windows 10. + var major = parseInt( version.split( "." )[ 0 ], 10 ); + os = major >= 13 ? "Windows 11" : ( major >= 1 ? "Windows 10" : "Windows" ); + } else if ( version && ( "macOS" === os || "Android" === os || "Chrome OS" === os ) ) { + os += " " + version.split( "." ).slice( 0, 2 ).join( "." ).replace( /\.0$/, "" ); + } + + return os ? browser + " on " + os : browser; + } + + function parseUserAgent( ua ) { + var browser = null; + var os = null; + var m; + + // iPadOS 13+ reports a Mac user agent; the touch screen gives it away. + var iPad = /iPad/.test( ua ) || ( "MacIntel" === navigator.platform && navigator.maxTouchPoints > 1 ); + + if ( iPad || /iPhone|iPod/.test( ua ) ) { + os = iPad ? "iPadOS" : "iOS"; + m = ua.match( /OS (\d+)[_.](\d+)/ ); + if ( m ) { + os += " " + m[ 1 ] + "." + m[ 2 ]; + } + if ( ( m = ua.match( /CriOS\/(\d+)/ ) ) ) { + browser = "Chrome " + m[ 1 ]; + } else if ( ( m = ua.match( /FxiOS\/(\d+)/ ) ) ) { + browser = "Firefox " + m[ 1 ]; + } else if ( ( m = ua.match( /EdgiOS\/(\d+)/ ) ) ) { + browser = "Edge " + m[ 1 ]; + } else if ( /OPiOS|OPT\//.test( ua ) ) { + browser = "Opera"; + } else if ( ( m = ua.match( /Version\/(\d+(?:\.\d+)?)/ ) ) ) { + browser = "Safari " + m[ 1 ]; + } else { + browser = "Safari"; + } + return browser + " on " + os; + } + + if ( ( m = ua.match( /Android (\d+(?:\.\d+)?)/ ) ) ) { + os = "Android " + m[ 1 ]; + } else if ( ( m = ua.match( /Windows NT (\d+\.\d+)/ ) ) ) { + // NT 10.0 covers both Windows 10 and 11 — the frozen user agent + // cannot tell them apart without client hints. + os = { "10.0": "Windows 10/11", "6.3": "Windows 8.1", "6.2": "Windows 8", "6.1": "Windows 7" }[ m[ 1 ] ] || "Windows"; + } else if ( /CrOS/.test( ua ) ) { + os = "Chrome OS"; + } else if ( ( m = ua.match( /Mac OS X (\d+[_.]\d+(?:[_.]\d+)?)/ ) ) ) { + var v = m[ 1 ].replace( /_/g, "." ); + // Browsers freeze the reported macOS version at 10.15.7 — treat it + // as "recent macOS" rather than reporting a 2019 system. + os = "10.15.7" === v ? "macOS" : "macOS " + v.split( "." ).slice( 0, 2 ).join( "." ); + } else if ( /Macintosh/.test( ua ) ) { + os = "macOS"; + } else if ( /Linux/.test( ua ) ) { + os = "Linux"; + } + + if ( ( m = ua.match( /Edg(?:e|A)?\/(\d+)/ ) ) ) { + browser = "Edge " + m[ 1 ]; + } else if ( ( m = ua.match( /OPR\/(\d+)/ ) ) ) { + browser = "Opera " + m[ 1 ]; + } else if ( ( m = ua.match( /SamsungBrowser\/(\d+)/ ) ) ) { + browser = "Samsung Internet " + m[ 1 ]; + } else if ( ( m = ua.match( /Firefox\/(\d+)/ ) ) ) { + browser = "Firefox " + m[ 1 ]; + } else if ( ( m = ua.match( /Chrome\/(\d+)/ ) ) ) { + browser = "Chrome " + m[ 1 ]; + } else if ( /Safari\//.test( ua ) && ( m = ua.match( /Version\/(\d+(?:\.\d+)?)/ ) ) ) { + browser = "Safari " + m[ 1 ]; + } + + if ( browser && os ) { + return browser + " on " + os; + } + return browser || os; + } + function copyText( text ) { if ( navigator.clipboard && navigator.clipboard.writeText ) { return navigator.clipboard.writeText( text ).catch( function(){ diff --git a/pirate-parrot.php b/pirate-parrot.php index 6c467df..30d41a6 100644 --- a/pirate-parrot.php +++ b/pirate-parrot.php @@ -141,8 +141,9 @@ function admin_enqueue_scripts() { 'pirate-parrot', 'pp', array( - 'nonce' => wp_create_nonce( 'parrot' ), - 'copied' => __( 'Copied!', 'pirate-parrot' ), + 'nonce' => wp_create_nonce( 'parrot' ), + 'copied' => __( 'Copied!', 'pirate-parrot' ), + 'generatedFrom' => __( 'Details generated from', 'pirate-parrot' ), ) );