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' ), ) );