From 5e3f48d741cefb6bfd79ae01f25b9374b096c59e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:16:34 -0600 Subject: [PATCH 1/7] [bump-version] Bump version to 1.3.0-RC1 (#432) Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: cdils <3099408+cdils@users.noreply.github.com> --- plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index bdfb20b0..043df1cd 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,7 @@ /** * Plugin Name: FAIR Connect - Federated and Independent Repositories * Description: Make your site more FAIR. - * Version: 1.2.2 + * Version: 1.3.0-RC1 * Author: FAIR Contributors * Author URI: https://fair.pm * Security: security@fair.pm @@ -20,7 +20,7 @@ namespace FAIR; -const VERSION = '1.2.2'; +const VERSION = '1.3.0-RC1'; const PLUGIN_DIR = __DIR__; const PLUGIN_FILE = __FILE__; From d52ee0fd7b12f005c9ee8cb63253f3610ed215e0 Mon Sep 17 00:00:00 2001 From: Carrie Dils Date: Tue, 10 Feb 2026 14:28:10 -0600 Subject: [PATCH 2/7] release: merge release_1.3.0 into development for RC testing (#433) Signed-off-by: John Blackbourn Signed-off-by: Andy Fragen Signed-off-by: Carrie Dils Signed-off-by: Norcross Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: joedolson Signed-off-by: Joe Dolson Signed-off-by: Shadi Sharaf Co-authored-by: Chuck Adams Co-authored-by: John Blackbourn Co-authored-by: Andy Fragen Co-authored-by: Norcross Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: rmccue <21655+rmccue@users.noreply.github.com> Co-authored-by: joedolson Co-authored-by: Joe Dolson Co-authored-by: Shady Sharaf Co-authored-by: cdils <3099408+cdils@users.noreply.github.com> --- .github/workflows/releases.yml | 12 + bin/bundle.sh | 16 ++ inc/default-repo/namespace.php | 1 + inc/namespace.php | 1 + inc/packages/namespace.php | 62 ++++- inc/updater/class-lite.php | 30 +- inc/updater/class-updater.php | 10 +- inc/updater/namespace.php | 72 ++++- inc/version-check/namespace.php | 472 +++++++++++++++++++++++++++++++- languages/fair.pot | 42 +-- 10 files changed, 669 insertions(+), 49 deletions(-) diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 49937bd6..b9b60a9a 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -37,6 +37,18 @@ jobs: /tmp/${{ github.event.repository.name }}-${{ steps.tag.outputs.tag }}.zip /tmp/fair-dist/* + - name: Upload to Fastly Object Storage + # note the plugin filename is always 'fair-connect', regardless of the repo name or slug + run: | + aws s3 cp /tmp/${{ github.event.repository.name }}-${{ steps.tag.outputs.tag }}.zip s3://download.fair.pm/release/fair-connect-${{ steps.tag.outputs.tag }}.zip + aws s3 cp --recursive --exclude '*' --include '*.zip' /tmp/fair-dist s3://download.fair.pm/release/ + env: + AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOAD_BUCKET_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOAD_BUCKET_SECRET_KEY }} + AWS_DEFAULT_REGION: 'us-west' + AWS_ENDPOINT_URL: 'https://us-west.object.fastlystorage.app' + AWS_REQUEST_CHECKSUM_CALCULATION: 'WHEN_REQUIRED' + - name: Build provenance attestation uses: actions/attest-build-provenance@v2 with: diff --git a/bin/bundle.sh b/bin/bundle.sh index 577ccfae..eabae922 100755 --- a/bin/bundle.sh +++ b/bin/bundle.sh @@ -19,6 +19,22 @@ touch /tmp/fair-dist/SHA1SUMS touch /tmp/fair-dist/SHA256SUMS touch /tmp/fair-dist/SHA384SUMS +# Auto-detect and hash plugin zip. +PLUGIN_VERSION=$(get_plugin_header "Version") +REPO_NAME=$(basename "$GITHUB_REPOSITORY") +PLUGIN_ZIP="/tmp/${REPO_NAME}-${PLUGIN_VERSION}.zip" + +if [ -f "$PLUGIN_ZIP" ]; then + echo "Hashing plugin zip: $PLUGIN_ZIP" >&2 + # Change to /tmp so hash files contain just filename, not full path + cd /tmp + md5sum -b "$(basename "$PLUGIN_ZIP")" >> /tmp/fair-dist/MD5SUMS + sha1sum -b "$(basename "$PLUGIN_ZIP")" >> /tmp/fair-dist/SHA1SUMS + sha256sum -b "$(basename "$PLUGIN_ZIP")" >> /tmp/fair-dist/SHA256SUMS + sha384sum -b "$(basename "$PLUGIN_ZIP")" >> /tmp/fair-dist/SHA384SUMS + cd - > /dev/null +fi + # Bundle our plugin first. [ -d /tmp/fair-temp ] && rm -rf /tmp/fair-temp mkdir -p /tmp/fair-temp/wordpress/wp-content/plugins/fair-plugin diff --git a/inc/default-repo/namespace.php b/inc/default-repo/namespace.php index 6719ede4..7cf89b7e 100644 --- a/inc/default-repo/namespace.php +++ b/inc/default-repo/namespace.php @@ -53,6 +53,7 @@ function replace_repo_api_urls( $status, $args, $url ) { if ( ! str_contains( $url, 'api.wordpress.org/plugins/' ) && ! str_contains( $url, 'api.wordpress.org/themes/' ) + && ! str_contains( $url, 'api.wordpress.org/translations/' ) && ! str_contains( $url, 'api.wordpress.org/core/version-check/' ) ) { return $status; diff --git a/inc/namespace.php b/inc/namespace.php index e12ed82d..de17542a 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -11,6 +11,7 @@ const CACHE_BASE = 'fair-'; const CACHE_LIFETIME = 12 * HOUR_IN_SECONDS; +const CACHE_LIFETIME_FAILURE = HOUR_IN_SECONDS; const NS_SEPARATOR = '\\'; /** diff --git a/inc/packages/namespace.php b/inc/packages/namespace.php index 9733ae4f..1dfff720 100644 --- a/inc/packages/namespace.php +++ b/inc/packages/namespace.php @@ -9,6 +9,7 @@ use const FAIR\CACHE_BASE; use const FAIR\CACHE_LIFETIME; +use const FAIR\CACHE_LIFETIME_FAILURE; use FAIR\Packages\DID\Document as DIDDocument; use FAIR\Packages\DID\PLC; use FAIR\Packages\DID\Web; @@ -22,12 +23,33 @@ const CACHE_KEY = CACHE_BASE . 'packages-'; const CACHE_METADATA_DOCUMENTS = CACHE_BASE . 'metadata-documents-'; const CACHE_RELEASE_PACKAGES = CACHE_BASE . 'release-packages'; +const CACHE_UPDATE_ERRORS = CACHE_BASE . 'update-errors-'; const CACHE_DID_FOR_INSTALL = 'fair-install-did'; const CONTENT_TYPE = 'application/json+fair'; const SERVICE_ID = 'FairPackageManagementRepo'; // phpcs:disable WordPress.NamingConventions.ValidVariableName +/** + * Cache an update error for a package. + * + * @param string $did DID of the package. + * @param WP_Error $error The error to cache. + */ +function cache_update_error( string $did, WP_Error $error ): void { + $error->add_data( [ 'timestamp' => time() ], $error->get_error_code() ); + set_site_transient( CACHE_UPDATE_ERRORS . $did, $error, CACHE_LIFETIME_FAILURE ); +} + +/** + * Clear a cached update error for a package. + * + * @param string $did DID of the package. + */ +function clear_update_error( string $did ): void { + delete_site_transient( CACHE_UPDATE_ERRORS . $did ); +} + /** * Bootstrap. * @@ -93,6 +115,12 @@ function get_did_hash( string $id ) { * @return DIDDocument|WP_Error */ function get_did_document( string $id ) { + // Check for cached error from previous failed request. + $cached_error = get_site_transient( CACHE_UPDATE_ERRORS . $id ); + if ( is_wp_error( $cached_error ) ) { + return $cached_error; + } + $cached = get_site_transient( CACHE_METADATA_DOCUMENTS . $id ); if ( $cached ) { return $cached; @@ -101,13 +129,18 @@ function get_did_document( string $id ) { // Parse the DID, then fetch the details. $did = parse_did( $id ); if ( is_wp_error( $did ) ) { + cache_update_error( $id, $did ); return $did; } $document = $did->fetch_document(); if ( is_wp_error( $document ) ) { + cache_update_error( $id, $document ); return $document; } + + // Clear any previous error on success. + clear_update_error( $id ); set_site_transient( CACHE_METADATA_DOCUMENTS . $id, $document, CACHE_LIFETIME ); return $document; @@ -177,20 +210,27 @@ function fetch_package_metadata( string $id ) { // Fetch data from the repository. $service = $document->get_service( SERVICE_ID ); if ( empty( $service ) ) { - return new WP_Error( 'fair.packages.fetch_metadata.no_service', __( 'DID is not a valid package to fetch metadata for.', 'fair' ) ); + $error = new WP_Error( 'fair.packages.fetch_metadata.no_service', __( 'DID is not a valid package to fetch metadata for.', 'fair' ) ); + cache_update_error( $id, $error ); + return $error; } $repo_url = $service->serviceEndpoint; - $metadata = fetch_metadata_doc( $repo_url ); + $metadata = fetch_metadata_doc( $repo_url, $id ); if ( is_wp_error( $metadata ) ) { return $metadata; } if ( $metadata->id !== $id ) { - return new WP_Error( 'fair.packages.fetch_metadata.mismatch', __( 'Fetched metadata does not match the requested DID.', 'fair' ) ); + $error = new WP_Error( 'fair.packages.fetch_metadata.mismatch', __( 'Fetched metadata does not match the requested DID.', 'fair' ) ); + cache_update_error( $id, $error ); + return $error; } + // Clear any previous error on success. + clear_update_error( $id ); + return $metadata; } @@ -198,9 +238,10 @@ function fetch_package_metadata( string $id ) { * Fetch the metadata document for a package. * * @param string $url URL for the metadata document. + * @param string $did DID of the package. * @return MetadataDocument|WP_Error */ -function fetch_metadata_doc( string $url ) { +function fetch_metadata_doc( string $url, string $did ) { $cache_key = CACHE_KEY . md5( $url ); $response = get_site_transient( $cache_key ); $response = fetch_metadata_from_local( $response, $url ); @@ -219,9 +260,15 @@ function fetch_metadata_doc( string $url ) { $response = wp_remote_get( $url, $options ); $code = wp_remote_retrieve_response_code( $response ); if ( is_wp_error( $response ) ) { + cache_update_error( $did, $response ); return $response; } elseif ( $code !== 200 ) { - return new WP_Error( 'fair.packages.metadata.failure', __( 'HTTP error code received', 'fair' ) ); + $error = new WP_Error( + 'fair.packages.metadata.http_error', + sprintf( __( 'HTTP %d error received', 'fair' ), $code ) + ); + cache_update_error( $did, $error ); + return $error; } // Reorder sections before caching. @@ -727,6 +774,9 @@ function cache_did_for_install( array $options ): array { $did = array_find_key( $releases, function ( $release ) use ( $options ) { + if ( ! is_array( $release->artifacts->package ) ) { + return false; + } $artifact = pick_artifact_by_lang( $release->artifacts->package ); return $artifact && $artifact->url === $options['package']; } @@ -754,7 +804,7 @@ function delete_cached_did_for_install(): void { * * This is commonly required for packages from Git hosts. * - * @param string $source Path of $source. + * @param string|WP_Error $source Path of $source, or a WP_Error object. * @param string $remote_source Path of $remote_source. * @param WP_Upgrader $upgrader An Upgrader object. * @param array $hook_extra Array of hook data. diff --git a/inc/updater/class-lite.php b/inc/updater/class-lite.php index ca54e93a..c9c67ebb 100644 --- a/inc/updater/class-lite.php +++ b/inc/updater/class-lite.php @@ -125,6 +125,13 @@ public function run() { ); $response = get_site_transient( "git-updater-lite_{$this->file}" ); if ( ! $response ) { + /* Apply filter to API URL. + * Add `channel=development` query arg to URL to get pre-release versions. + * + * @param string $url The API URL. + * @param string $slug The plugin/theme slug + */ + $url = apply_filters( 'git_updater_lite_api_url', $url, $this->slug ); $response = wp_remote_get( $url ); if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) === 404 ) { return $response; @@ -136,11 +143,9 @@ public function run() { } $this->api_data->file = $this->file; - /* - * Set transient for 5 minutes as AWS sets 5 minute timeout - * for release asset redirect. - */ - set_site_transient( "git-updater-lite_{$this->file}", $this->api_data, 5 * \MINUTE_IN_SECONDS ); + // Set timeout for transient via filter. + $timeout = apply_filters( 'git_updater_lite_transient_timeout', 6 * HOUR_IN_SECONDS, $this->file ); + set_site_transient( "git-updater-lite_{$this->file}", $this->api_data, $timeout ); } else { if ( property_exists( $response, 'error' ) ) { return new WP_Error( 'repo-no-exist', 'Specified repo does not exist' ); @@ -178,18 +183,23 @@ function () { /** * Correctly rename dependency for activation. * - * @param string $source Path of $source. - * @param string $remote_source Path of $remote_source. - * @param WP_Upgrader $upgrader An Upgrader object. - * @param array $hook_extra Array of hook data. + * @param string|WP_Error $source Path of $source, or a WP_Error object. + * @param string $remote_source Path of $remote_source. + * @param WP_Upgrader $upgrader An Upgrader object. + * @param array $hook_extra Array of hook data. * * @throws TypeError If the type of $upgrader is not correct. * * @return string|WP_Error */ - public function upgrader_source_selection( string $source, string $remote_source, WP_Upgrader $upgrader, $hook_extra = null ) { + public function upgrader_source_selection( $source, string $remote_source, WP_Upgrader $upgrader, $hook_extra = null ) { global $wp_filesystem; + // Exit early for errors. + if ( is_wp_error( $source ) ) { + return $source; + } + $new_source = $source; // Exit if installing. diff --git a/inc/updater/class-updater.php b/inc/updater/class-updater.php index aa6f415b..611f6417 100644 --- a/inc/updater/class-updater.php +++ b/inc/updater/class-updater.php @@ -12,6 +12,7 @@ use stdClass; use Theme_Upgrader; use TypeError; +use WP_Error; use WP_Upgrader; /** @@ -149,7 +150,7 @@ public function load_hooks() { /** * Correctly rename dependency for activation. * - * @param string $source Path of $source. + * @param string|WP_Error $source Path of $source, or a WP_Error object. * @param string $remote_source Path of $remote_source. * @param WP_Upgrader $upgrader An Upgrader object. * @param array $hook_extra Array of hook data. @@ -158,9 +159,14 @@ public function load_hooks() { * * @return string|WP_Error */ - public function upgrader_source_selection( string $source, string $remote_source, WP_Upgrader $upgrader, $hook_extra = null ) { + public function upgrader_source_selection( $source, string $remote_source, WP_Upgrader $upgrader, $hook_extra = null ) { global $wp_filesystem; + // Exit early for errors. + if ( is_wp_error( $source ) ) { + return $source; + } + $new_source = $source; // Exit if installing. diff --git a/inc/updater/namespace.php b/inc/updater/namespace.php index 3db5114b..361b61dc 100644 --- a/inc/updater/namespace.php +++ b/inc/updater/namespace.php @@ -7,8 +7,10 @@ namespace FAIR\Updater; +use const FAIR\CACHE_LIFETIME_FAILURE; use const FAIR\Packages\CACHE_DID_FOR_INSTALL; use const FAIR\Packages\CACHE_RELEASE_PACKAGES; +use const FAIR\Packages\CACHE_UPDATE_ERRORS; use FAIR\Packages; use function FAIR\is_wp_cli; use Plugin_Upgrader; @@ -22,6 +24,7 @@ */ function bootstrap() { add_action( 'init', __NAMESPACE__ . '\\run' ); + add_action( 'admin_init', __NAMESPACE__ . '\\register_plugin_row_hooks' ); } /** @@ -79,14 +82,79 @@ function run() { } } +/** + * Register hooks to display update errors below plugin rows. + */ +function register_plugin_row_hooks(): void { + $packages = get_packages(); + $plugins = $packages['plugins'] ?? []; + + foreach ( $plugins as $did => $path ) { + $plugin_file = plugin_basename( $path ); + add_action( + "after_plugin_row_{$plugin_file}", + function ( $file, $plugin_data, $status ) use ( $did ) { + display_plugin_update_error( $file, $plugin_data, $status, $did ); + }, + 10, + 3 + ); + } +} + +/** + * Display a cached update error below the plugin row. + * + * @param string $plugin_file Path to the plugin file relative to the plugins directory. + * @param array $plugin_data An array of plugin data. + * @param string $status Status filter currently applied to the plugin list. + * @param string $did The DID of the plugin. + */ +function display_plugin_update_error( $plugin_file, $plugin_data, $status, $did ): void { + $error = get_site_transient( CACHE_UPDATE_ERRORS . $did ); + if ( ! is_wp_error( $error ) ) { + return; + } + + $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' ); + $colspan = $wp_list_table->get_column_count(); + + // Calculate time remaining until retry. + $error_data = $error->get_error_data(); + $timestamp = $error_data['timestamp'] ?? 0; + $retry_time = $timestamp + CACHE_LIFETIME_FAILURE; + $time_remaining = human_time_diff( time(), $retry_time ); + + $message = sprintf( + /* translators: %1$s: Error message, %2$s: Time period */ + __( 'Error: %1$s. Update checks paused for %2$s.', 'fair' ), + $error->get_error_message(), + $time_remaining, + ); + + $active_class = is_plugin_active( $plugin_file ) ? ' active' : ''; + + printf( + ' + +

%4$s

+ + ', + esc_attr( $active_class ), + esc_attr( sanitize_title( $plugin_file ) ), + esc_attr( $colspan ), + esc_html( $message ), + ); +} + /** * Download a package with signature verification. * - * @param bool|string|WP_Error $reply Whether to proceed with the download, the path to the downloaded package, or an existing WP_Error object. Default true. + * @param bool|string|WP_Error $reply Whether to proceed with the download, the path to the downloaded package, or an existing WP_Error object. * @param string $package The URI of the package. If this is the full path to an existing local file, it will be returned untouched. * @param WP_Upgrader $upgrader The WP_Upgrader instance. * @param array $hook_extra Extra hook data. - * @return true|WP_Error True if the signature is valid, otherwise WP_Error. + * @return string|WP_Error The package path if the signature is valid, otherwise WP_Error. */ function verify_signature_on_download( $reply, string $package, WP_Upgrader $upgrader, $hook_extra ) { static $has_run = []; diff --git a/inc/version-check/namespace.php b/inc/version-check/namespace.php index 13c2269e..2149827a 100644 --- a/inc/version-check/namespace.php +++ b/inc/version-check/namespace.php @@ -14,7 +14,7 @@ * * DO NOT EDIT THIS CONSTANT MANUALLY. */ -const BROWSER_REGEX = '/Edge?\/14[0-2]\.0(\.\d+|)|Firefox\/(140\.0|14[3-8]\.0)(\.\d+|)|Chrom(ium|e)\/(109\.0|1{2}2\.0|12[56]\.0|130\.0|134\.0|1(39|4[0-6])\.0)(\.\d+|)|(Maci|X1{2}).+ Version\/26\.[12]([,.]\d+|)( \(\w+\)|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/12[12]\.0\.\d+|(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS|CPU iPad OS)[ +]+(18[._][56]|26[._][01])([._]\d+|)|Opera Mini|Android:?[ /-]142(\.0|)(\.\d+|)|Mobile Safari.+OPR\/8(0\.){2}\d+|Android.+Firefox\/14{2}\.0(\.\d+|)|Android.+Chrom(ium|e)\/142\.0(\.\d+|)|Android.+(UC? ?Browser|UCWEB|U3)[ /]?1(5\.){2}\d+|SamsungBrowser\/2[89]\.0|Android.+MQ{2}Browser\/14(\.9|)(\.\d+|)|K[Aa][Ii]OS\/(2\.5|3\.[01])(\.\d+|)/'; +const BROWSER_REGEX = '/Edge?\/14[34]\.0(\.\d+|)|Firefox\/(140\.0|1(4[6-9]|50)\.0)(\.\d+|)|Chrom(ium|e)\/(109\.0|1{2}2\.0|131\.0|13{2}\.0|139\.0|14[2-7]\.0)(\.\d+|)|(Maci|X1{2}).+ Version\/26\.[1-3]([,.]\d+|)( \(\w+\)|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/12[45]\.0\.\d+|(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS|CPU iPad OS)[ +]+(18[._][5-7]|26[._][1-3])([._]\d+|)|Opera Mini|Android:?[ /-]14{2}(\.0|)(\.\d+|)|Mobile Safari.+OPR\/8(0\.){2}\d+|Android.+Firefox\/147\.0(\.\d+|)|Android.+Chrom(ium|e)\/14{2}\.0(\.\d+|)|Android.+(UC? ?Browser|UCWEB|U3)[ /]?1(5\.){2}\d+|SamsungBrowser\/2[89]\.0|Android.+MQ{2}Browser\/14(\.9|)(\.\d+|)|K[Aa][Ii]OS\/(2\.5|3\.[01])(\.\d+|)/'; /** * The latest branch of PHP which WordPress.org recommends. @@ -87,23 +87,27 @@ function get_browser_check_response( string $agent ) { // Switch delimiter to avoid conflicts. $regex = '#' . trim( BROWSER_REGEX, '/' ) . '#'; $supported = preg_match( $regex, $agent, $matches ); + $data = parse_user_agent( $agent ); + + $default_data = [ + 'platform' => _x( 'your platform', 'operating system check', 'fair' ), + 'name' => _x( 'your browser', 'browser version check', 'fair' ), + 'version' => '', + 'current_version' => '', + 'upgrade' => ! $supported, + 'insecure' => ! $supported, + 'update_url' => 'https://browsehappy.com/', + 'img_src' => '', + 'img_src_ssl' => '', + ]; + $data = array_merge( $default_data, $data ); return [ 'response' => [ 'code' => 200, 'message' => 'OK', ], - 'body' => json_encode( [ - 'platform' => _x( 'your platform', 'operating system check', 'fair' ), - 'name' => _x( 'your browser', 'browser version check', 'fair' ), - 'version' => '', - 'current_version' => '', - 'upgrade' => ! $supported, - 'insecure' => ! $supported, - 'update_url' => 'https://browsehappy.com/', - 'img_src' => '', - 'img_src_ssl' => '', - ] ), + 'body' => json_encode( $data ), 'headers' => [], 'cookies' => [], 'http_response_code' => 200, @@ -262,3 +266,447 @@ function get_server_check_response( string $version ) { 'http_response_code' => 200, ]; } + +/** + * Returns current version numbers for all browsers. + * + * These are for major release branches, not full build numbers. + * Firefox 3.6, 4, etc., not Chrome 11.0.696.65. + * + * @return array Associative array of browser names with their respective + * current (or somewhat current) version number. + */ +function get_browser_current_versions() { + return [ + 'Chrome' => '18', // Lowest version at the moment (mobile). + 'Firefox' => '56', + 'Microsoft Edge' => '15.15063', + 'Opera' => '12.18', + 'Safari' => '11', + 'Internet Explorer' => '11', + ]; +} + +/** + * Returns browser data for a given browser. + * + * @param string|false $browser The name of the browser. Default false. + * @return false|array|object { + * Array of data objects about browsers. False if the browser is unknown. + * + * @type string $name Name of the browser. + * @type string $url The home URL for the browser. + * @type string $img_src The non-HTTPs URL for the browser's logo image. + * @type string $img_src_ssl The HTTPS URL for the browser's logo image. + * } + */ +function get_browser_data( $browser = false ) { + + $data = [ + 'Internet Explorer' => (object) [ + 'name' => 'Internet Explorer', + 'url' => 'https://support.microsoft.com/help/17621/internet-explorer-downloads', + ], + 'Edge' => (object) [ + 'name' => 'Microsoft Edge', + 'url' => 'https://www.microsoft.com/edge', + ], + 'Firefox' => (object) [ + 'name' => 'Mozilla Firefox', + 'url' => 'https://www.mozilla.org/firefox/', + ], + 'Safari' => (object) [ + 'name' => 'Safari', + 'url' => 'https://www.apple.com/safari/', + ], + 'Opera' => (object) [ + 'name' => 'Opera', + 'url' => 'https://www.opera.com/', + ], + 'Chrome' => (object) [ + 'name' => 'Google Chrome', + 'url' => 'https://www.google.com/chrome', + ], + ]; + + if ( false === $browser ) { + return $data; + } + + if ( ! isset( $data[ $browser ] ) ) { + return false; + } + + return $data[ $browser ]; +} + +/** + * Returns an associative array of explicit browser token names and their + * associated info. + * + * Explicit tokens are tokens that, if present, indicate a specific browser. + * + * If a browser is not identified by an explicit token, or s special + * handling not supported by the default handler, then a new conditional block + * for the browser instead needs to be added in parse_user_agent(). + * + * In any case, the browser token name also needs to be added to the regex for + * browser tokens in parse_user_agent(). + * + * @return array { + * Associative array of browser tokens and their associated data. + * + * @type array $data { + * Associative array of browser data. All are optional. + * + * @type string $name Name of browser, if it differs from the + * token name. Default is token name. + * @type bool $use_version Should the 'Version' token, if present, + * supercede the version associated with the + * browser token? Default false. + * @type bool $mobile Does the browser signify the platform is + * mobile (for situations where it may no + * already be apparent)? Default false. + * @type string $platform The name of the platform, to supercede + * whatever platform may have been detected. + * Default empty string. + * } + * } + */ +function get_explicit_browser_tokens() { + return [ + 'Camino' => [], + 'Chromium' => [], + 'Edge' => [ + 'name' => 'Microsoft Edge', + ], + 'Kindle' => [ + 'name' => 'Kindle Browser', + 'use_version' => true, + ], + 'Konqueror' => [], + 'konqueror' => [ + 'name' => 'Konqueror', + ], + 'NokiaBrowser' => [ + 'name' => 'Nokia Browser', + 'mobile' => true, + ], + 'Opera Mini' => [ // Must be before 'Opera'. + 'mobile' => true, + 'use_version' => true, + ], + 'Opera' => [ + 'use_version' => true, + ], + 'OPR' => [ + 'name' => 'Opera', + 'use_version' => true, + ], + 'PaleMoon' => [ + 'name' => 'Pale Moon', + ], + 'QQBrowser' => [ + 'name' => 'QQ Browser', + ], + 'RockMelt' => [], + 'SamsungBrowser' => [ + 'name' => 'Samsung Browser', + ], + 'SeaMonkey' => [], + 'Silk' => [ + 'name' => 'Amazon Silk', + ], + 'S40OviBrowser' => [ + 'name' => 'Ovi Browser', + 'mobile' => true, + 'platform' => 'Symbian', + ], + 'UCBrowser' => [ // Must be before 'UCWEB'. + 'name' => 'UC Browser', + ], + 'UCWEB' => [ + 'name' => 'UC Browser', + ], + 'Vivaldi' => [], + 'IEMobile' => [ // Keep last just in case. + 'name' => 'Internet Explorer Mobile', + ], + ]; +} + +/** + * Parses a user agent string into its important parts. + * + * @param string $user_agent The user agent string for a browser. + * @return array { + * Array containing data based on the parsing of the user agent. + * + * @type string $platform The platform running the browser. + * @type string $name The name of the browser. + * @type string $version The reported version of the browser. + * @type string $update_url The URL to obtain the update for the browser. + * @type string $img_src The non-HTTPS URL for the browser's logo image. + * @type string $img_src_ssl The HTTPS URL for the browser's logo image. + * @type string $current_version The current latest version of the browser. + * @type bool $upgrade Is there an update available for the browser? + * @type bool $insecure Is the browser insecure? + * @type bool $mobile Is the browser on a mobile platform? + * } + */ +function parse_user_agent( $user_agent ) { + $data = [ + 'name' => '', + 'version' => '', + 'platform' => '', + 'update_url' => '', + 'img_src' => '', + 'img_src_ssl' => '', + 'current_version' => '', + 'upgrade' => false, + 'insecure' => false, + 'mobile' => false, + ]; + $mobile_device = ''; + + /** + * Identify platform/OS in user-agent string. + * '/(?P' // Capture subpattern matches into 'platform' array. + * . 'Windows Phone( OS)?|Symbian|SymbOS|Android|iPhone' // Platform tokens. + * . '|iPad|Windows|Linux|Macintosh|FreeBSD|OpenBSD' // More platform tokens. + * . '|SunOS|RIM Tablet OS|PlayBook' // More platform tokens. + * . ')' + * . '(?:' + * . ' (NT|amd64|armv7l|zvav)' // Possibly followed by specific modifiers/specifiers. + * . ')*' + * . '(?:' + * . ' [ix]?[0-9._]+' // Possibly followed by architecture modifier (e.g. x86_64). + * . '(\-[0-9a-z\.\-]+)?' // Possibly followed by a hypenated version number. + * . ')*' + * . '(;|\))' // Ending in a semi-colon or close parenthesis. + * . '/im', // Case insensitive, multiline. + */ + if ( preg_match( + '/(?PWindows Phone( OS)?|Symbian|SymbOS|Android|iPhone|iPad|Windows|Linux|Macintosh|FreeBSD|OpenBSD|SunOS|RIM Tablet OS|PlayBook)(?: (NT|amd64|armv7l|zvav))*(?: [ix]?[0-9._]+(\-[0-9a-z\.\-]+)?)*(;|\))/im', + $user_agent, + $regs + ) ) { + $data['platform'] = $regs['platform']; + } + + /** + * Find tokens of interest in user-agent string. + * + * '%(?P' // Capture subpattern matches into the 'name' array. + * . 'Opera Mini|Opera|OPR|Edge|UCBrowser|UCWEB' // Browser tokens. + * . '|QQBrowser|SymbianOS|Symbian|S40OviBrowser' // More browser tokens. + * . '|Trident|Silk|Konqueror|PaleMoon|Puffin' // More browser tokens. + * . '|SeaMonkey|Vivaldi|Camino|Chromium|Kindle|Firefox' // More browser tokens. + * . '|SamsungBrowser|(?:Mobile )?Safari|NokiaBrowser' // More browser tokens. + * . '|MSIE|RockMelt|AppleWebKit|Chrome|IEMobile' // More browser tokens. + * . '|Version' // Version token. + * . ')' + * . '(?:' + * . '[/ ]' // Forward slash or space. + * . ')' + * . '(?P' // Capture subpattern matches into 'version' array. + * . '[0-9.]+' // One or more numbers and/or decimal points. + * . ')' + * . '%im', // Case insensitive, multiline. + */ + preg_match_all( + '%(?POpera Mini|Opera|OPR|Edge|UCBrowser|UCWEB|QQBrowser|SymbianOS|Symbian|S40OviBrowser|Trident|Silk|Konqueror|PaleMoon|Puffin|SeaMonkey|Vivaldi|Camino|Chromium|Kindle|Firefox|SamsungBrowser|(?:Mobile )?Safari|NokiaBrowser|MSIE|RockMelt|AppleWebKit|Chrome|IEMobile|Version)(?:[/ ])(?P[0-9.]+)%im', + $user_agent, + $result, + PREG_PATTERN_ORDER + ); + + // Create associative array with tokens as keys and versions as values. + $tokens = array_combine( array_reverse( $result['name'] ), array_reverse( $result['version'] ) ); + + // Properly set platform if Android is actually being reported. + if ( 'Linux' === $data['platform'] && false !== strpos( $user_agent, 'Android' ) ) { + if ( strpos( $user_agent, 'Kindle' ) ) { + $data['platform'] = 'Fire OS'; + } else { + $data['platform'] = 'Android'; + } + } elseif ( 'Windows Phone' === $data['platform'] ) { + // Normalize Windows Phone OS name when "OS" is omitted. + $data['platform'] = 'Windows Phone OS'; + } elseif ( in_array( $data['platform'], [ 'Symbian', 'SymbOS' ] ) || ! empty( $tokens['SymbianOS'] ) || ! empty( $tokens['Symbian'] ) ) { + // Standardize Symbian OS name. + if ( ! in_array( $data['platform'], [ 'Symbian', 'SymbOS' ] ) ) { + unset( $tokens['SymbianOS'] ); + unset( $tokens['Symbian'] ); + } + $data['platform'] = 'Symbian'; + } elseif ( ! $data['platform'] && preg_match( '/BlackBerry|Nokia|SonyEricsson/', $user_agent, $matches ) ) { + // Generically detect some mobile devices. + $data['platform'] = 'Mobile'; + $mobile_device = $matches[0]; + } + + // Flag known mobile platforms as mobile. + if ( in_array( $data['platform'], [ 'Android', 'Fire OS', 'iPad', 'iPhone', 'Mobile', 'PlayBook', 'RIM Tablet OS', 'Symbian', 'Windows Phone OS' ] ) ) { + $data['mobile'] = true; + } + + // If Version/x.x.x was specified in UA string store it and ignore it. + if ( ! empty( $tokens['Version'] ) ) { + $version = $tokens['Version']; + unset( $tokens['Version'] ); + } + + $explicit_tokens = get_explicit_browser_tokens(); + + // No indentifiers provided. + if ( ! $tokens ) { + if ( 'BlackBerry' === $mobile_device ) { + $data['name'] = 'BlackBerry Browser'; + } else { + $data['name'] = 'unknown'; + } + } elseif ( $found = array_intersect( array_keys( $explicit_tokens ), array_keys( $tokens ) ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure + // Explicitly identified browser (info defined above in $explicit_tokens). + $token = reset( $found ); + + $data['name'] = $explicit_tokens[ $token ]['name'] ?? $token; + $data['version'] = $tokens[ $token ]; + if ( empty( $explicit_tokens[ $token ]['use_version'] ) ) { + $version = ''; + } + if ( ! empty( $explicit_tokens[ $token ]['mobile'] ) ) { + $data['mobile'] = true; + } + if ( ! empty( $explicit_tokens[ $token ]['platform'] ) ) { + $data['platform'] = $explicit_tokens[ $token ]['platform']; + } + } elseif ( ! empty( $tokens['Puffin'] ) ) { + // Puffin. + $data['name'] = 'Puffin'; + $data['version'] = $tokens['Puffin']; + $version = ''; + // If not an already-identified mobile platform, set it as such. + if ( ! $data['mobile'] ) { + $data['mobile'] = true; + $data['platform'] = ''; + } + } elseif ( ! empty( $tokens['Trident'] ) ) { + // Trident (Internet Explorer). + // IE 8-10 more reliably report version via Trident token than MSIE token. + // IE 11 uses Trident token without an MSIE token. + // https://msdn.microsoft.com/library/hh869301(v=vs.85).aspx. + $data['name'] = 'Internet Explorer'; + $trident_ie_mapping = [ + '4.0' => '8.0', + '5.0' => '9.0', + '6.0' => '10.0', + '7.0' => '11.0', + ]; + $ver = $tokens['Trident']; + $data['version'] = $trident_ie_mapping[ $ver ] ?? $ver; + } elseif ( ! empty( $tokens['MSIE'] ) ) { + // Internet Explorer (pre v8.0). + $data['name'] = 'Internet Explorer'; + $data['version'] = $tokens['MSIE']; + } elseif ( ! empty( $tokens['AppleWebKit'] ) ) { + // AppleWebKit-emulating browsers. + if ( ! empty( $tokens['Mobile Safari'] ) ) { + if ( ! empty( $tokens['Chrome'] ) ) { + $data['name'] = 'Chrome'; + $version = $tokens['Chrome']; + } elseif ( 'Android' === $data['platform'] ) { + $data['name'] = 'Android Browser'; + } elseif ( 'Fire OS' === $data['platform'] ) { + $data['name'] = 'Kindle Browser'; + } elseif ( false !== strpos( $user_agent, 'BlackBerry' ) || false !== strpos( $user_agent, 'BB10' ) ) { + $data['name'] = 'BlackBerry Browser'; + $data['mobile'] = true; + + if ( false !== stripos( $user_agent, 'BB10' ) ) { + $tokens['Mobile Safari'] = ''; + $version = ''; + } + } else { + $data['name'] = 'Mobile Safari'; + } + } elseif ( ! empty( $tokens['Chrome'] ) ) { + $data['name'] = 'Chrome'; + $version = ''; + } elseif ( ! empty( $data['platform'] ) && 'PlayBook' == $data['platform'] ) { + $data['name'] = 'PlayBook'; + } elseif ( ! empty( $tokens['Safari'] ) ) { + if ( 'Android' === $data['platform'] ) { + $data['name'] = 'Android Browser'; + } elseif ( 'Symbian' === $data['platform'] ) { + $data['name'] = 'Nokia Browser'; + $tokens['Safari'] = ''; + } else { + $data['name'] = 'Safari'; + } + } else { + $data['name'] = 'unknown'; + $tokens['AppleWebKit'] = ''; + $version = ''; + } + $data['version'] = $tokens[ $data['name'] ] ?? ''; + } else { + // Fall back to whatever is being reported. + $ordered_tokens = array_reverse( $tokens ); + $data['version'] = reset( $ordered_tokens ); + $data['name'] = key( $ordered_tokens ); + } + + // Set the platform for Amazon-related browsers. + if ( in_array( $data['name'], [ 'Amazon Silk', 'Kindle Browser' ] ) ) { + $data['platform'] = 'Fire OS'; + $data['mobile'] = true; + } + + // If Version/x.x.x was specified in UA string. + if ( ! empty( $version ) ) { + $data['version'] = $version; + } + + if ( $data['mobile'] ) { + // Generically set "Mobile" as the platform if a platform hasn't been set. + if ( ! $data['platform'] ) { + $data['platform'] = 'Mobile'; + } + + // Don't fetch additional browser data for mobile platform browsers at this time. + return $data; + } + + $browser_data = get_browser_data( $data['name'] ); + $data['update_url'] = $browser_data ? $browser_data->url : ''; + $data['current_version'] = get_browser_version_from_name( $data['name'] ); + $data['upgrade'] = ( ! empty( $data['current_version'] ) && version_compare( $data['version'], $data['current_version'], '<' ) ); + + if ( 'Internet Explorer' === $data['name'] ) { + $data['insecure'] = true; + $data['upgrade'] = true; + } elseif ( 'Firefox' === $data['name'] && version_compare( $data['version'], '52', '<' ) ) { + $data['insecure'] = true; + } elseif ( 'Opera' === $data['name'] && version_compare( $data['version'], '12.18', '<' ) ) { + $data['insecure'] = true; + } elseif ( 'Safari' === $data['name'] && version_compare( $data['version'], '10', '<' ) ) { + $data['insecure'] = true; + } + + return $data; +} + +/** + * Returns the current version for the given browser. + * + * @param string $name The name of the browser. + * @return string The version for the browser or an empty string if an + * unknown browser. + */ +function get_browser_version_from_name( $name ) { + $versions = get_browser_current_versions(); + + return isset( $versions[ $name ] ) ? $versions[ $name ] : ''; +} diff --git a/languages/fair.pot b/languages/fair.pot index f69dec09..91e798da 100644 --- a/languages/fair.pot +++ b/languages/fair.pot @@ -1,15 +1,15 @@ -# Copyright (C) 2025 FAIR Contributors +# Copyright (C) 2026 FAIR Contributors # This file is distributed under the GPLv2. msgid "" msgstr "" -"Project-Id-Version: FAIR Connect - Federated and Independent Repositories 1.1.0\n" +"Project-Id-Version: FAIR Connect - Federated and Independent Repositories 1.2.2\n" "Report-Msgid-Bugs-To: https://github.com/fairpm/fair-plugin/issues\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2025-12-11T04:32:53+00:00\n" +"POT-Creation-Date: 2026-01-07T22:20:21+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: fair\n" @@ -319,68 +319,76 @@ msgstr "" msgid "The PLC directory did not return the DID that was sent or the DID was invalid." msgstr "" -#: inc/packages/namespace.php:47 +#: inc/packages/namespace.php:50 msgid "ID is not a valid DID." msgstr "" -#: inc/packages/namespace.php:52 +#: inc/packages/namespace.php:55 msgid "DID could not be parsed as a URI." msgstr "" -#: inc/packages/namespace.php:63 +#: inc/packages/namespace.php:66 msgid "Unsupported DID method." msgstr "" -#: inc/packages/namespace.php:128 +#: inc/packages/namespace.php:146 +msgid "The package's file list could not be retrieved." +msgstr "" + +#: inc/packages/namespace.php:162 +msgid "No FAIR packages were found." +msgstr "" + +#: inc/packages/namespace.php:180 msgid "DID is not a valid package to fetch metadata for." msgstr "" -#: inc/packages/namespace.php:139 +#: inc/packages/namespace.php:191 msgid "Fetched metadata does not match the requested DID." msgstr "" -#: inc/packages/namespace.php:172 +#: inc/packages/namespace.php:224 msgid "HTTP error code received" msgstr "" -#: inc/packages/namespace.php:252 +#: inc/packages/namespace.php:306 msgid "DID does not contain valid signing keys." msgstr "" -#: inc/packages/namespace.php:262 +#: inc/packages/namespace.php:316 msgid "No releases found in the repository." msgstr "" -#: inc/packages/namespace.php:827 +#: inc/packages/namespace.php:954 msgctxt "alias validation error" msgid "Multiple aliases set in DID; packages may only have a single alias" msgstr "" -#: inc/packages/namespace.php:837 +#: inc/packages/namespace.php:964 msgctxt "alias validation error" msgid "Invalid FAIR alias format" msgstr "" -#: inc/packages/namespace.php:846 +#: inc/packages/namespace.php:973 msgctxt "alias validation error" msgid "FAIR alias format exceeds valid domain length" msgstr "" #. translators: %s: domain -#: inc/packages/namespace.php:859 +#: inc/packages/namespace.php:986 #, php-format msgctxt "alias validation error" msgid "Missing verification record for \"%s\"" msgstr "" #. translators: %s: domain -#: inc/packages/namespace.php:873 +#: inc/packages/namespace.php:1000 #, php-format msgctxt "alias validation error" msgid "Verification record for \"%s\" is invalid" msgstr "" -#: inc/packages/namespace.php:884 +#: inc/packages/namespace.php:1011 msgctxt "alias validation error" msgid "DID in validation record does not match" msgstr "" From c33f7f8b26f3e013df808cb04dcee1a69e035a90 Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Wed, 11 Feb 2026 10:59:22 -0800 Subject: [PATCH 3/7] Pass at validation of hash (#436) Signed-off-by: Andy Fragen --- bin/bundle.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bin/bundle.sh b/bin/bundle.sh index eabae922..6257f9c8 100755 --- a/bin/bundle.sh +++ b/bin/bundle.sh @@ -70,6 +70,12 @@ for VERSION in $AVAILABLE_VERSIONS; do curl -sSL "$WP_ZIP_URL" -o "$WP_ZIP_FILE" EXPECTED_HASH=$(curl -sSL "$WP_ZIP_URL.sha1") + # Skip if we can't get a valid hash. + if ![[ $EXPECTED_HASH =~ ^[a-z0-9]{40}$ ]]; then + echo "Failed to fetch valid hash for $VERSION" >&2 + continue + fi + # Verify the checksum. # (sha1 is suboptimal, but it's all we've got.) echo " Verifying checksum" >&2 From c9a2984224fd7467a78c66f863ff485fddaa462a Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Wed, 11 Feb 2026 11:38:39 -0800 Subject: [PATCH 4/7] Validate bundle hash (#437) Signed-off-by: Andy Fragen --- bin/bundle.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/bundle.sh b/bin/bundle.sh index 6257f9c8..fedcb914 100755 --- a/bin/bundle.sh +++ b/bin/bundle.sh @@ -71,7 +71,7 @@ for VERSION in $AVAILABLE_VERSIONS; do EXPECTED_HASH=$(curl -sSL "$WP_ZIP_URL.sha1") # Skip if we can't get a valid hash. - if ![[ $EXPECTED_HASH =~ ^[a-z0-9]{40}$ ]]; then + if [[ ! "$EXPECTED_HASH" =~ ^[a-z0-9]{40}$ ]]; then echo "Failed to fetch valid hash for $VERSION" >&2 continue fi From 401719a11db8ebc9bace1a8f6badf5ed989bd533 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:50:27 -0600 Subject: [PATCH 5/7] [bump-version] Bump version to 1.3.0-RC4 (#448) Signed-off-by: Chuck Adams Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: Carrie Dils Co-authored-by: Chuck Adams Co-authored-by: Carrie Dils Co-authored-by: cdils <3099408+cdils@users.noreply.github.com> --- plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.php b/plugin.php index 043df1cd..7d8120cf 100644 --- a/plugin.php +++ b/plugin.php @@ -2,7 +2,7 @@ /** * Plugin Name: FAIR Connect - Federated and Independent Repositories * Description: Make your site more FAIR. - * Version: 1.3.0-RC1 + * Version: 1.3.0-RC4 * Author: FAIR Contributors * Author URI: https://fair.pm * Security: security@fair.pm @@ -20,7 +20,7 @@ namespace FAIR; -const VERSION = '1.3.0-RC1'; +const VERSION = '1.3.0-RC4'; const PLUGIN_DIR = __DIR__; const PLUGIN_FILE = __FILE__; From 9602a34c593d55e3bde2673fb634519fcadc881e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:33:53 -0600 Subject: [PATCH 6/7] [browserslist] Update browser regex (#451) Signed-off-by: Chuck Adams Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: Carrie Dils Co-authored-by: Chuck Adams Co-authored-by: Carrie Dils Co-authored-by: chuckadams <3925+chuckadams@users.noreply.github.com> --- inc/version-check/namespace.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/version-check/namespace.php b/inc/version-check/namespace.php index 2149827a..a988676a 100644 --- a/inc/version-check/namespace.php +++ b/inc/version-check/namespace.php @@ -14,7 +14,7 @@ * * DO NOT EDIT THIS CONSTANT MANUALLY. */ -const BROWSER_REGEX = '/Edge?\/14[34]\.0(\.\d+|)|Firefox\/(140\.0|1(4[6-9]|50)\.0)(\.\d+|)|Chrom(ium|e)\/(109\.0|1{2}2\.0|131\.0|13{2}\.0|139\.0|14[2-7]\.0)(\.\d+|)|(Maci|X1{2}).+ Version\/26\.[1-3]([,.]\d+|)( \(\w+\)|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/12[45]\.0\.\d+|(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS|CPU iPad OS)[ +]+(18[._][5-7]|26[._][1-3])([._]\d+|)|Opera Mini|Android:?[ /-]14{2}(\.0|)(\.\d+|)|Mobile Safari.+OPR\/8(0\.){2}\d+|Android.+Firefox\/147\.0(\.\d+|)|Android.+Chrom(ium|e)\/14{2}\.0(\.\d+|)|Android.+(UC? ?Browser|UCWEB|U3)[ /]?1(5\.){2}\d+|SamsungBrowser\/2[89]\.0|Android.+MQ{2}Browser\/14(\.9|)(\.\d+|)|K[Aa][Ii]OS\/(2\.5|3\.[01])(\.\d+|)/'; +const BROWSER_REGEX = '/Edge?\/14[3-5]\.0(\.\d+|)|Firefox\/(140\.0|1(4[6-9]|50)\.0)(\.\d+|)|Chrom(ium|e)\/(109\.0|1{2}2\.0|131\.0|13{2}\.0|139\.0|14[2-8]\.0)(\.\d+|)|(Maci|X1{2}).+ Version\/26\.[2-4]([,.]\d+|)( \(\w+\)|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/12[45]\.0\.\d+|(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS|CPU iPad OS)[ +]+(18[._][5-7]|26[._][1-4])([._]\d+|)|Opera Mini|Android:?[ /-]145(\.0|)(\.\d+|)|Mobile Safari.+OPR\/8(0\.){2}\d+|Android.+Firefox\/147\.0(\.\d+|)|Android.+Chrom(ium|e)\/145\.0(\.\d+|)|Android.+(UC? ?Browser|UCWEB|U3)[ /]?1(5\.){2}\d+|SamsungBrowser\/2[89]\.0|Android.+MQ{2}Browser\/14(\.9|)(\.\d+|)|K[Aa][Ii]OS\/(2\.5|3\.[01])(\.\d+|)/'; /** * The latest branch of PHP which WordPress.org recommends. From e8811190cdfb59c3582ebb2ce5d869fad7b68488 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:34:24 -0600 Subject: [PATCH 7/7] Generate POT - 2026-02-16-175601 (#445) Signed-off-by: Chuck Adams Signed-off-by: cdils Signed-off-by: Carrie Dils Co-authored-by: Chuck Adams Co-authored-by: cdils --- .github/workflows/update-browserslist.yaml | 2 -- languages/fair.pot | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/update-browserslist.yaml b/.github/workflows/update-browserslist.yaml index 55b25b0b..1cae411b 100644 --- a/.github/workflows/update-browserslist.yaml +++ b/.github/workflows/update-browserslist.yaml @@ -2,8 +2,6 @@ name: Update browserslist regex on: workflow_dispatch: - schedule: - - cron: "0 0 * * 1" # Every Monday at midnight jobs: update-browserslist-regex: diff --git a/languages/fair.pot b/languages/fair.pot index 91e798da..22b2eedc 100644 --- a/languages/fair.pot +++ b/languages/fair.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-01-07T22:20:21+00:00\n" +"POT-Creation-Date: 2026-02-16T17:56:00+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: fair\n"