o the file
* @since 1.0.3
* @since-jetpack 5.6.0
*/
public static function get_file_url_for_environment( $min_path, $non_min_path, $package_path = '' ) {
$path = ( Jetpack_Constants::is_defined( 'SCRIPT_DEBUG' ) && Jetpack_Constants::get_constant( 'SCRIPT_DEBUG' ) )
? $non_min_path
: $min_path;
/*
* If the path is actually a full URL, keep that.
* We look for a host value, since enqueues are sometimes without a scheme.
*/
$file_parts = wp_parse_url( $path );
if ( ! empty( $file_parts['host'] ) ) {
$url = $path;
} else {
$plugin_path = empty( $package_path ) ? Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) : $package_path;
$url = plugins_url( $path, $plugin_path );
}
/**
* Filters the URL for a file passed through the get_file_url_for_environment function.
*
* @since 1.0.3
*
* @package assets
*
* @param string $url The URL to the file.
* @param string $min_path The minified path.
* @param string $non_min_path The non-minified path.
*/
return apply_filters( 'jetpack_get_file_for_environment', $url, $min_path, $non_min_path );
}
/**
* Passes an array of URLs to wp_resource_hints.
*
* @since 1.5.0
*
* @param string|array $urls URLs to hint.
* @param string $type One of the supported resource types: dns-prefetch (default), preconnect, prefetch, or prerender.
*/
public static function add_resource_hint( $urls, $type = 'dns-prefetch' ) {
add_filter(
'wp_resource_hints',
function ( $hints, $resource_type ) use ( $urls, $type ) {
if ( $resource_type === $type ) {
// Type casting to array required since the function accepts a single string.
foreach ( (array) $urls as $url ) {
$hints[] = $url;
}
}
return $hints;
},
10,
2
);
}
/**
* Serve a WordPress.com static resource via a randomized wp.com subdomain.
*
* @since 1.9.0
*
* @param string $url WordPress.com static resource URL.
*
* @return string $url
*/
public static function staticize_subdomain( $url ) {
// Extract hostname from URL.
$host = wp_parse_url( $url, PHP_URL_HOST );
// Explode hostname on '.'.
$exploded_host = explode( '.', $host );
// Retrieve the name and TLD.
if ( count( $exploded_host ) > 1 ) {
$name = $exploded_host[ count( $exploded_host ) - 2 ];
$tld = $exploded_host[ count( $exploded_host ) - 1 ];
// Rebuild domain excluding subdomains.
$domain = $name . '.' . $tld;
} else {
$domain = $host;
}
// Array of Automattic domains.
$domains_allowed = array( 'wordpress.com', 'wp.com' );
// Return $url if not an Automattic domain.
if ( ! in_array( $domain, $domains_allowed, true ) ) {
return $url;
}
if ( \is_ssl() ) {
return preg_replace( '|https?://[^/]++/|', 'https://s-ssl.wordpress.com/', $url );
}
/*
* Generate a random subdomain id by taking the modulus of the crc32 value of the URL.
* Valid values are 0, 1, and 2.
*/
$static_counter = abs( crc32( basename( $url ) ) % 3 );
return preg_replace( '|://[^/]+?/|', "://s$static_counter.wp.com/", $url );
}
/**
* Resolve '.' and '..' components in a path or URL.
*
* @since 1.12.0
* @param string $path Path or URL.
* @return string Normalized path or URL.
*/
public static function normalize_path( $path ) {
$parts = wp_parse_url( $path );
if ( ! isset( $parts['path'] ) ) {
return $path;
}
$ret = '';
$ret .= isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
$ret .= $parts['user'] ?? '';
$ret .= isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
$ret .= '@';
}
$ret .= $parts['host'] ?? '';
$ret .= isset( $parts['port'] ) ? ':' . $parts['port'] : '';
$pp = explode( '/', $parts['path'] );
if ( '' === $pp[0] ) {
$ret .= '/';
array_shift( $pp );
}
$i = 0;
while ( $i < count( $pp ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
if ( '' === $pp[ $i ] || '.' === $pp[ $i ] || 0 === $i && '..' === $pp[ $i ] ) {
array_splice( $pp, $i, 1 );
} elseif ( '..' === $pp[ $i ] ) {
array_splice( $pp, --$i, 2 );
} else {
++$i;
}
}
$ret .= implode( '/', $pp );
$ret .= isset( $parts['query'] ) ? '?' . $parts['query'] : '';
$ret .= isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
return $ret;
}
// endregion .
// ////////////////////
// region Webpack-built script registration
/**
* Register a Webpack-built script.
*
* Our Webpack-built scripts tend to need a bunch of boilerplate:
* - A call to `Assets::get_file_url_for_environment()` for possible debugging.
* - A call to `wp_register_style()` for extracted CSS, possibly with detection of RTL.
* - Loading of dependencies and version provided by `@wordpress/dependency-extraction-webpack-plugin`.
* - Avoiding WPCom's broken minifier.
*
* This wrapper handles all of that.
*
* @since 1.12.0
* @since 2.1.0 Add a new `strategy` option to leverage WP >= 6.3 script strategy feature. The `async` option is deprecated.
* @param string $handle Name of the script. Should be unique across both scripts and styles.
* @param string $path Minimized script path.
* @param string $relative_to File that `$path` is relative to. Pass `__FILE__`.
* @param array $options Additional options:
* - `asset_path`: (string|null) `.asset.php` to load. Default is to base it on `$path`.
* - `async`: (bool) Set true to register the script as deferred, like `Assets::enqueue_async_script()`. Deprecated in favor of `strategy`.
* - `css_dependencies`: (string[]) Additional style dependencies to queue.
* - `css_path`: (string|null) `.css` to load. Default is to base it on `$path`.
* - `dependencies`: (string[]) Additional script dependencies to queue.
* - `enqueue`: (bool) Set true to enqueue the script immediately.
* - `in_footer`: (bool) Set true to register script for the footer.
* - `media`: (string) Media for the css file. Default 'all'.
* - `minify`: (bool|null) Set true to pass `minify=true` in the query string, or `null` to suppress the normal `minify=false`.
* - `nonmin_path`: (string) Non-minified script path.
* - `strategy`: (string) Specify a script strategy to use, eg. `defer` or `async`. Default is `""`.
* - `textdomain`: (string) Text domain for the script. Required if the script depends on wp-i18n.
* - `version`: (string) Override the version from the `asset_path` file.
* @phan-param array{asset_path?:?string,async?:bool,css_dependencies?:string[],css_path?:?string,dependencies?:string[],enqueue?:bool,in_footer?:bool,media?:string,minify?:?bool,nonmin_path?:string,strategy?:string,textdomain?:string,version?:string} $options
* @throws \InvalidArgumentException If arguments are invalid.
*/
public static function register_script( $handle, $path, $relative_to, array $options = array() ) {
if ( substr( $path, -3 ) !== '.js' ) {
throw new \InvalidArgumentException( '$path must end in ".js"' );
}
if ( isset( $options['async'] ) ) {
_deprecated_argument( __METHOD__, '2.1.0', 'The `async` option is deprecated in favor of `strategy`' );
}
$dir = dirname( $relative_to );
$base = substr( $path, 0, -3 );
$options += array(
'asset_path' => "$base.asset.php",
'async' => false,
'css_dependencies' => array(),
'css_path' => "$base.css",
'dependencies' => array(),
'enqueue' => false,
'in_footer' => false,
'media' => 'all',
'minify' => false,
'strategy' => '',
'textdomain' => null,
);
'@phan-var array{asset_path:?string,async:bool,css_dependencies:string[],css_path:?string,dependencies:string[],enqueue:bool,in_footer:bool,media:string,minify:?bool,nonmin_path?:string,strategy:string,textdomain:string,version?:string} $options'; // Phan gets confused by the array addition.
if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && substr( $options['css_path'], -4 ) !== '.css' ) {
throw new \InvalidArgumentException( '$options[\'css_path\'] must end in ".css"' );
}
if ( isset( $options['nonmin_path'] ) ) {
$url = self::get_file_url_for_environment( $path, $options['nonmin_path'], $relative_to );
} else {
$url = plugins_url( $path, $relative_to );
}
$url = self::normalize_path( $url );
if ( null !== $options['minify'] ) {
$url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url );
}
if ( $options['asset_path'] && file_exists( "$dir/{$options['asset_path']}" ) ) {
$asset = require "$dir/{$options['asset_path']}"; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.NotAbsolutePath
$options['dependencies'] = array_merge( $asset['dependencies'], $options['dependencies'] );
$options['css_dependencies'] = array_merge(
array_filter(
$asset['dependencies'],
function ( $d ) {
return wp_style_is( $d, 'registered' );
}
),
$options['css_dependencies']
);
$ver = $options['version'] ?? $asset['version'];
} else {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$ver = $options['version'] ?? @filemtime( "$dir/$path" );
}
if ( $options['async'] && '' === $options['strategy'] ) { // Handle the deprecated `async` option
$options['strategy'] = 'defer';
}
wp_register_script(
$handle,
$url,
$options['dependencies'],
$ver,
array(
'in_footer' => $options['in_footer'],
'strategy' => $options['strategy'],
)
);
if ( $options['textdomain'] ) {
// phpcs:ignore Jetpack.Functions.I18n.DomainNotLiteral
wp_set_script_translations( $handle, $options['textdomain'] );
} elseif ( in_array( 'wp-i18n', $options['dependencies'], true ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s is the script handle. */
esc_html( sprintf( __( 'Script "%s" depends on wp-i18n but does not specify "textdomain"', 'jetpack-assets' ), $handle ) ),
''
);
}
if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && file_exists( "$dir/{$options['css_path']}" ) ) {
$csspath = $options['css_path'];
if ( is_rtl() ) {
$rtlcsspath = substr( $csspath, 0, -4 ) . '.rtl.css';
if ( file_exists( "$dir/$rtlcsspath" ) ) {
$csspath = $rtlcsspath;
}
}
$url = self::normalize_path( plugins_url( $csspath, $relative_to ) );
if ( null !== $options['minify'] ) {
$url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url );
}
wp_register_style( $handle, $url, $options['css_dependencies'], $ver, $options['media'] );
wp_script_add_data( $handle, 'Jetpack::Assets::hascss', true );
} else {
wp_script_add_data( $handle, 'Jetpack::Assets::hascss', false );
}
if ( $options['enqueue'] ) {
self::enqueue_script( $handle );
}
}
/**
* Enqueue a script registered with `Assets::register_script`.
*
* @since 1.12.0
* @param string $handle Name of the script. Should be unique across both scripts and styles.
*/
public static function enqueue_script( $handle ) {
wp_enqueue_script( $handle );
if ( wp_scripts()->get_data( $handle, 'Jetpack::Assets::hascss' ) ) {
wp_enqueue_style( $handle );
}
}
/**
* 'wp_default_scripts' action handler.
*
* This registers the `wp-jp-i18n-loader` script for use by Webpack bundles built with
* `@automattic/i18n-loader-webpack-plugin`.
*
* @since 1.14.0
* @param \WP_Scripts $wp_scripts WP_Scripts instance.
*/
public static function wp_default_scripts_hook( $wp_scripts ) {
$data = array(
'baseUrl' => false,
'locale' => determine_locale(),
'domainMap' => array(),
'domainPaths' => array(),
);
$lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
$content_dir = Jetpack_Constants::get_constant( 'WP_CONTENT_DIR' );
$abspath = Jetpack_Constants::get_constant( 'ABSPATH' );
// Note: str_starts_with() is not used here, as wp-includes/compat.php may not be loaded at this point.
if ( strpos( $lang_dir, $content_dir ) === 0 ) {
$data['baseUrl'] = content_url( substr( trailingslashit( $lang_dir ), strlen( trailingslashit( $content_dir ) ) ) );
} elseif ( strpos( $lang_dir, $abspath ) === 0 ) {
$data['baseUrl'] = site_url( substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) );
}
foreach ( self::$domain_map as $from => list( $to, $type, , $path ) ) {
$data['domainMap'][ $from ] = ( 'core' === $type ? '' : "{$type}/" ) . $to;
if ( '' !== $path ) {
$data['domainPaths'][ $from ] = trailingslashit( $path );
}
}
/**
* Filters the i18n state data for use by Webpack bundles built with
* `@automattic/i18n-loader-webpack-plugin`.
*
* @since 1.14.0
* @package assets
* @param array $data The state data to generate. Expected fields are:
* - `baseUrl`: (string|false) The URL to the languages directory. False if no URL could be determined.
* - `locale`: (string) The locale for the page.
* - `domainMap`: (string[]) A mapping from Composer package textdomains to the corresponding
* `plugins/textdomain` or `themes/textdomain` (or core `textdomain`, but that's unlikely).
* - `domainPaths`: (string[]) A mapping from Composer package textdomains to the corresponding package
* paths.
*/
$data = apply_filters( 'jetpack_i18n_state', $data );
// Can't use self::register_script(), this action is called too early.
if ( file_exists( __DIR__ . '/../build/i18n-loader.asset.php' ) ) {
$path = '../build/i18n-loader.js';
$asset = require __DIR__ . '/../build/i18n-loader.asset.php';
} else {
$path = 'js/i18n-loader.js';
$asset = array(
'dependencies' => array( 'wp-i18n' ),
'version' => filemtime( __DIR__ . "/$path" ),
);
}
$url = self::normalize_path( plugins_url( $path, __FILE__ ) );
$url = add_query_arg( 'minify', 'true', $url );
$handle = 'wp-jp-i18n-loader';
$wp_scripts->add( $handle, $url, $asset['dependencies'], $asset['version'] );
// Ensure the script is loaded in the footer and deferred.
$wp_scripts->add_data( $handle, 'group', 1 );
if ( ! is_array( $data ) ||
! isset( $data['baseUrl'] ) || ! ( is_string( $data['baseUrl'] ) || false === $data['baseUrl'] ) ||
! isset( $data['locale'] ) || ! is_string( $data['locale'] ) ||
! isset( $data['domainMap'] ) || ! is_array( $data['domainMap'] ) ||
! isset( $data['domainPaths'] ) || ! is_array( $data['domainPaths'] )
) {
$wp_scripts->add_inline_script( $handle, 'console.warn( "I18n state deleted by jetpack_i18n_state hook" );' );
} elseif ( ! $data['baseUrl'] ) {
$wp_scripts->add_inline_script( $handle, 'console.warn( "Failed to determine languages base URL. Is WP_LANG_DIR in the WordPress root?" );' );
} else {
$data['domainMap'] = (object) $data['domainMap']; // Ensure it becomes a json object.
$data['domainPaths'] = (object) $data['domainPaths']; // Ensure it becomes a json object.
$wp_scripts->add_inline_script( $handle, 'wp.jpI18nLoader.state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';' );
}
// Deprecated state module: Depend on wp-i18n to ensure global `wp` exists and because anything needing this will need that too.
$wp_scripts->add( 'wp-jp-i18n-state', false, array( 'wp-deprecated', $handle ) );
$wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.deprecated( "wp-jp-i18n-state", { alternative: "wp-jp-i18n-loader" } );' );
$wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.jpI18nState = wp.jpI18nLoader.state;' );
// Register the React JSX runtime script - used as a polyfill until we can update JSX transforms. See https://github.com/Automattic/jetpack/issues/38424.
// @todo Remove this when we drop support for WordPress 6.5, as well as the script inclusion in test_wp_default_scripts_hook.
$jsx_url = self::normalize_path( plugins_url( '../build/react-jsx-runtime.js', __FILE__ ) );
$wp_scripts->add( 'react-jsx-runtime', $jsx_url, array( 'react' ), '18.3.1', true );
$wp_scripts->add_data( 'react-jsx-runtime', 'group', 1 );
}
// endregion .
// ////////////////////
// region Textdomain aliasing
/**
* Register a textdomain alias.
*
* Composer packages included in plugins will likely not use the textdomain of the plugin, while
* WordPress's i18n infrastructure will include the translations in the plugin's domain. This
* allows for mapping the package's domain to the plugin's.
*
* Since multiple plugins may use the same package, we include the package's version here so
* as to choose the most recent translations (which are most likely to match the package
* selected by jetpack-autoloader).
*
* @since 1.15.0
* @param string $from Domain to alias.
* @param string $to Domain to alias it to.
* @param string $totype What is the target of the alias: 'plugins', 'themes', or 'core'.
* @param string $ver Version of the `$from` domain.
* @param string $path Path to prepend when lazy-loading from JavaScript.
* @throws InvalidArgumentException If arguments are invalid.
*/
public static function alias_textdomain( $from, $to, $totype, $ver, $path = '' ) {
if ( ! in_array( $totype, array( 'plugins', 'themes', 'core' ), true ) ) {
throw new InvalidArgumentException( 'Type must be "plugins", "themes", or "core"' );
}
if (
did_action( 'wp_default_scripts' ) &&
// Don't complain during plugin activation.
! defined( 'WP_SANDBOX_SCRAPING' )
) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: wp_default_scripts. 2: Name of the domain being aliased. */
esc_html__( 'Textdomain aliases should be registered before the %1$s hook. This notice was triggered by the %2$s domain.', 'jetpack-assets' ),
'wp_default_scripts
',
'' . esc_html( $from ) . '
'
),
''
);
}
if ( empty( self::$domain_map[ $from ] ) ) {
self::init_domain_map_hooks( $from, array() === self::$domain_map );
self::$domain_map[ $from ] = array( $to, $totype, $ver, $path );
} elseif ( Semver::compare( $ver, self::$domain_map[ $from ][2] ) > 0 ) {
self::$domain_map[ $from ] = array( $to, $totype, $ver, $path );
}
}
/**
* Register textdomain aliases from a mapping file.
*
* The mapping file is simply a PHP file that returns an array
* with the following properties:
* - 'domain': String, `$to`
* - 'type': String, `$totype`
* - 'packages': Array, mapping `$from` to `array( 'path' => $path, 'ver' => $ver )` (or to the string `$ver` for back compat).
*
* @since 1.15.0
* @param string $file Mapping file.
*/
public static function alias_textdomains_from_file( $file ) {
$data = require $file;
foreach ( $data['packages'] as $from => $fromdata ) {
if ( ! is_array( $fromdata ) ) {
$fromdata = array(
'path' => '',
'ver' => $fromdata,
);
}
self::alias_textdomain( $from, $data['domain'], $data['type'], $fromdata['ver'], $fromdata['path'] );
}
}
/**
* Register the hooks for textdomain aliasing.
*
* @param string $domain Domain to alias.
* @param bool $firstcall If this is the first call.
*/
private static function init_domain_map_hooks( $domain, $firstcall ) {
// If WordPress's plugin API is available already, use it. If not,
// drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`.
if ( function_exists( 'add_filter' ) ) {
$add_filter = 'add_filter';
} else {
$add_filter = function ( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
global $wp_filter;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_filter[ $hook_name ][ $priority ][] = array(
'accepted_args' => $accepted_args,
'function' => $callback,
);
};
}
$add_filter( "gettext_{$domain}", array( self::class, 'filter_gettext' ), 10, 3 );
$add_filter( "ngettext_{$domain}", array( self::class, 'filter_ngettext' ), 10, 5 );
$add_filter( "gettext_with_context_{$domain}", array( self::class, 'filter_gettext_with_context' ), 10, 4 );
$add_filter( "ngettext_with_context_{$domain}", array( self::class, 'filter_ngettext_with_context' ), 10, 6 );
if ( $firstcall ) {
$add_filter( 'load_script_translation_file', array( self::class, 'filter_load_script_translation_file' ), 10, 3 );
}
}
/**
* Filter for `gettext`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $text Text to translate.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_gettext( $translation, $text, $domain ) {
if ( $translation === $text ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$newtext = __( $text, self::$domain_map[ $domain ][0] );
if ( $newtext !== $text ) {
return $newtext;
}
}
return $translation;
}
/**
* Filter for `ngettext`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $single The text to be used if the number is singular.
* @param string $plural The text to be used if the number is plural.
* @param int $number The number to compare against to use either the singular or plural form.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_ngettext( $translation, $single, $plural, $number, $domain ) {
if ( $translation === $single || $translation === $plural ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _n( $single, $plural, $number, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `gettext_with_context`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $text Text to translate.
* @param string $context Context information for the translators.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_gettext_with_context( $translation, $text, $context, $domain ) {
if ( $translation === $text ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _x( $text, $context, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `ngettext_with_context`.
*
* @since 1.15.0
* @param string $translation Translated text.
* @param string $single The text to be used if the number is singular.
* @param string $plural The text to be used if the number is plural.
* @param int $number The number to compare against to use either the singular or plural form.
* @param string $context Context information for the translators.
* @param string $domain Text domain.
* @return string Translated text.
*/
public static function filter_ngettext_with_context( $translation, $single, $plural, $number, $context, $domain ) {
if ( $translation === $single || $translation === $plural ) {
// phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
$translation = _nx( $single, $plural, $number, $context, self::$domain_map[ $domain ][0] );
}
return $translation;
}
/**
* Filter for `load_script_translation_file`.
*
* @since 1.15.0
* @param string|false $file Path to the translation file to load. False if there isn't one.
* @param string $handle Name of the script to register a translation domain to.
* @param string $domain The text domain.
*/
public static function filter_load_script_translation_file( $file, $handle, $domain ) {
if ( false !== $file && isset( self::$domain_map[ $domain ] ) && ! is_readable( $file ) ) {
// Determine the part of the filename after the domain.
$suffix = basename( $file );
$l = strlen( $domain );
if ( substr( $suffix, 0, $l ) !== $domain || '-' !== $suffix[ $l ] ) {
return $file;
}
$suffix = substr( $suffix, $l );
$lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
// Look for replacement files.
list( $newdomain, $type ) = self::$domain_map[ $domain ];
$newfile = $lang_dir . ( 'core' === $type ? '/' : "/{$type}/" ) . $newdomain . $suffix;
if ( is_readable( $newfile ) ) {
return $newfile;
}
}
return $file;
}
// endregion .
}
// Enable section folding in vim:
// vim: foldmarker=//\ region,//\ endregion foldmethod=marker
// .
);
throw static::createException($class, $message, static::INTERFACE_NOT_IMPLEMENTED, $propertyPath, ['interface' => $interfaceName]);
}
} catch (\ReflectionException $e) {
$message = \sprintf(
static::generateMessage($message ?: 'Class "%s" failed reflection.'),
static::stringify($class)
);
throw static::createException($class, $message, static::INTERFACE_NOT_IMPLEMENTED, $propertyPath, ['interface' => $interfaceName]);
}
return true;
}
/**
* Assert that the given string is a valid json string.
*
* NOTICE:
* Since this does a json_decode to determine its validity
* you probably should consider, when using the variable
* content afterwards, just to decode and check for yourself instead
* of using this assertion.
*
* @param mixed $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function isJsonString($value, $message = null, $propertyPath = null)
{
if (null === \json_decode($value) && JSON_ERROR_NONE !== \json_last_error()) {
$message = \sprintf(
static::generateMessage($message ?: 'Value "%s" is not a valid JSON string.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_JSON_STRING, $propertyPath);
}
return true;
}
/**
* Assert that the given string is a valid UUID.
*
* Uses code from {@link https://github.com/ramsey/uuid} that is MIT licensed.
*
* @param string $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function uuid($value, $message = null, $propertyPath = null)
{
$value = \str_replace(['urn:', 'uuid:', '{', '}'], '', $value);
if ('00000000-0000-0000-0000-000000000000' === $value) {
return true;
}
if (!\preg_match('/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/', $value)) {
$message = \sprintf(
static::generateMessage($message ?: 'Value "%s" is not a valid UUID.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_UUID, $propertyPath);
}
return true;
}
/**
* Assert that the given string is a valid E164 Phone Number.
*
* @see https://en.wikipedia.org/wiki/E.164
*
* @param string $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function e164($value, $message = null, $propertyPath = null)
{
if (!\preg_match('/^\+?[1-9]\d{1,14}$/', $value)) {
$message = \sprintf(
static::generateMessage($message ?: 'Value "%s" is not a valid E164.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_E164, $propertyPath);
}
return true;
}
/**
* Assert that the count of countable is equal to count.
*
* @param array|\Countable|\ResourceBundle|\SimpleXMLElement $countable
* @param int $count
* @param string|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function count($countable, $count, $message = null, $propertyPath = null)
{
if ($count !== \count($countable)) {
$message = \sprintf(
static::generateMessage($message ?: 'List does not contain exactly %d elements (%d given).'),
static::stringify($count),
static::stringify(\count($countable))
);
throw static::createException($countable, $message, static::INVALID_COUNT, $propertyPath, ['count' => $count]);
}
return true;
}
/**
* Assert that the countable have at least $count elements.
*
* @param array|\Countable|\ResourceBundle|\SimpleXMLElement $countable
* @param int $count
* @param string|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function minCount($countable, $count, $message = null, $propertyPath = null)
{
if ($count > \count($countable)) {
$message = \sprintf(
static::generateMessage($message ?: 'List should have at least %d elements, but has %d elements.'),
static::stringify($count),
static::stringify(\count($countable))
);
throw static::createException($countable, $message, static::INVALID_MIN_COUNT, $propertyPath, ['count' => $count]);
}
return true;
}
/**
* Assert that the countable have at most $count elements.
*
* @param array|\Countable|\ResourceBundle|\SimpleXMLElement $countable
* @param int $count
* @param string|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function maxCount($countable, $count, $message = null, $propertyPath = null)
{
if ($count < \count($countable)) {
$message = \sprintf(
static::generateMessage($message ?: 'List should have at most %d elements, but has %d elements.'),
static::stringify($count),
static::stringify(\count($countable))
);
throw static::createException($countable, $message, static::INVALID_MAX_COUNT, $propertyPath, ['count' => $count]);
}
return true;
}
/**
* static call handler to implement:
* - "null or assertion" delegation
* - "all" delegation.
*
* @param string $method
* @param array $args
*
* @return bool|mixed
*/
public static function __callStatic($method, $args)
{
if (0 === \strpos($method, 'nullOr')) {
if (!\array_key_exists(0, $args)) {
throw new BadMethodCallException('Missing the first argument.');
}
if (null === $args[0]) {
return true;
}
$method = \substr($method, 6);
return \call_user_func_array([\get_called_class(), $method], $args);
}
if (0 === \strpos($method, 'all')) {
if (!\array_key_exists(0, $args)) {
throw new BadMethodCallException('Missing the first argument.');
}
static::isTraversable($args[0]);
$method = \substr($method, 3);
$values = \array_shift($args);
$calledClass = \get_called_class();
foreach ($values as $value) {
\call_user_func_array([$calledClass, $method], \array_merge([$value], $args));
}
return true;
}
throw new BadMethodCallException('No assertion Assertion#'.$method.' exists.');
}
/**
* Determines if the values array has every choice as key and that this choice has content.
*
* @param array $values
* @param array $choices
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function choicesNotEmpty(array $values, array $choices, $message = null, $propertyPath = null)
{
static::notEmpty($values, $message, $propertyPath);
foreach ($choices as $choice) {
static::notEmptyKey($values, $choice, $message, $propertyPath);
}
return true;
}
/**
* Determines that the named method is defined in the provided object.
*
* @param string $value
* @param mixed $object
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function methodExists($value, $object, $message = null, $propertyPath = null)
{
static::isObject($object, $message, $propertyPath);
if (!\method_exists($object, $value)) {
$message = \sprintf(
static::generateMessage($message ?: 'Expected "%s" does not exist in provided object.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_METHOD, $propertyPath, ['object' => \get_class($object)]);
}
return true;
}
/**
* Determines that the provided value is an object.
*
* @param mixed $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function isObject($value, $message = null, $propertyPath = null)
{
if (!\is_object($value)) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is not a valid object.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_OBJECT, $propertyPath);
}
return true;
}
/**
* Determines if the value is less than given limit.
*
* @param mixed $value
* @param mixed $limit
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function lessThan($value, $limit, $message = null, $propertyPath = null)
{
if ($value >= $limit) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is not less than "%s".'),
static::stringify($value),
static::stringify($limit)
);
throw static::createException($value, $message, static::INVALID_LESS, $propertyPath, ['limit' => $limit]);
}
return true;
}
/**
* Determines if the value is less or equal than given limit.
*
* @param mixed $value
* @param mixed $limit
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function lessOrEqualThan($value, $limit, $message = null, $propertyPath = null)
{
if ($value > $limit) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is not less or equal than "%s".'),
static::stringify($value),
static::stringify($limit)
);
throw static::createException($value, $message, static::INVALID_LESS_OR_EQUAL, $propertyPath, ['limit' => $limit]);
}
return true;
}
/**
* Determines if the value is greater than given limit.
*
* @param mixed $value
* @param mixed $limit
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function greaterThan($value, $limit, $message = null, $propertyPath = null)
{
if ($value <= $limit) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is not greater than "%s".'),
static::stringify($value),
static::stringify($limit)
);
throw static::createException($value, $message, static::INVALID_GREATER, $propertyPath, ['limit' => $limit]);
}
return true;
}
/**
* Determines if the value is greater or equal than given limit.
*
* @param mixed $value
* @param mixed $limit
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function greaterOrEqualThan($value, $limit, $message = null, $propertyPath = null)
{
if ($value < $limit) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is not greater or equal than "%s".'),
static::stringify($value),
static::stringify($limit)
);
throw static::createException($value, $message, static::INVALID_GREATER_OR_EQUAL, $propertyPath, ['limit' => $limit]);
}
return true;
}
/**
* Assert that a value is greater or equal than a lower limit, and less than or equal to an upper limit.
*
* @param mixed $value
* @param mixed $lowerLimit
* @param mixed $upperLimit
* @param string $message
* @param string $propertyPath
*
* @return bool
*/
public static function between($value, $lowerLimit, $upperLimit, $message = null, $propertyPath = null)
{
if ($lowerLimit > $value || $value > $upperLimit) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is neither greater than or equal to "%s" nor less than or equal to "%s".'),
static::stringify($value),
static::stringify($lowerLimit),
static::stringify($upperLimit)
);
throw static::createException($value, $message, static::INVALID_BETWEEN, $propertyPath, ['lower' => $lowerLimit, 'upper' => $upperLimit]);
}
return true;
}
/**
* Assert that a value is greater than a lower limit, and less than an upper limit.
*
* @param mixed $value
* @param mixed $lowerLimit
* @param mixed $upperLimit
* @param string $message
* @param string $propertyPath
*
* @return bool
*/
public static function betweenExclusive($value, $lowerLimit, $upperLimit, $message = null, $propertyPath = null)
{
if ($lowerLimit >= $value || $value >= $upperLimit) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is neither greater than "%s" nor less than "%s".'),
static::stringify($value),
static::stringify($lowerLimit),
static::stringify($upperLimit)
);
throw static::createException($value, $message, static::INVALID_BETWEEN_EXCLUSIVE, $propertyPath, ['lower' => $lowerLimit, 'upper' => $upperLimit]);
}
return true;
}
/**
* Assert that extension is loaded.
*
* @param mixed $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function extensionLoaded($value, $message = null, $propertyPath = null)
{
if (!\extension_loaded($value)) {
$message = \sprintf(
static::generateMessage($message ?: 'Extension "%s" is required.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_EXTENSION, $propertyPath);
}
return true;
}
/**
* Assert that date is valid and corresponds to the given format.
*
* @param string $value
* @param string $format supports all of the options date(), except for the following:
* N, w, W, t, L, o, B, a, A, g, h, I, O, P, Z, c, r
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*
* @see http://php.net/manual/function.date.php#refsect1-function.date-parameters
*/
public static function date($value, $format, $message = null, $propertyPath = null)
{
static::string($value, $message, $propertyPath);
static::string($format, $message, $propertyPath);
$dateTime = \DateTime::createFromFormat('!'.$format, $value);
if (false === $dateTime || $value !== $dateTime->format($format)) {
$message = \sprintf(
static::generateMessage($message ?: 'Date "%s" is invalid or does not match format "%s".'),
static::stringify($value),
static::stringify($format)
);
throw static::createException($value, $message, static::INVALID_DATE, $propertyPath, ['format' => $format]);
}
return true;
}
/**
* Assert that the value is an object, or a class that exists.
*
* @param mixed $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function objectOrClass($value, $message = null, $propertyPath = null)
{
if (!\is_object($value)) {
static::classExists($value, $message, $propertyPath);
}
return true;
}
/**
* Assert that the value is an object or class, and that the property exists.
*
* @param mixed $value
* @param string $property
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function propertyExists($value, $property, $message = null, $propertyPath = null)
{
static::objectOrClass($value);
if (!\property_exists($value, $property)) {
$message = \sprintf(
static::generateMessage($message ?: 'Class "%s" does not have property "%s".'),
static::stringify($value),
static::stringify($property)
);
throw static::createException($value, $message, static::INVALID_PROPERTY, $propertyPath, ['property' => $property]);
}
return true;
}
/**
* Assert that the value is an object or class, and that the properties all exist.
*
* @param mixed $value
* @param array $properties
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function propertiesExist($value, array $properties, $message = null, $propertyPath = null)
{
static::objectOrClass($value);
static::allString($properties, $message, $propertyPath);
$invalidProperties = [];
foreach ($properties as $property) {
if (!\property_exists($value, $property)) {
$invalidProperties[] = $property;
}
}
if ($invalidProperties) {
$message = \sprintf(
static::generateMessage($message ?: 'Class "%s" does not have these properties: %s.'),
static::stringify($value),
static::stringify(\implode(', ', $invalidProperties))
);
throw static::createException($value, $message, static::INVALID_PROPERTY, $propertyPath, ['properties' => $properties]);
}
return true;
}
/**
* Assert comparison of two versions.
*
* @param string $version1
* @param string $operator
* @param string $version2
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function version($version1, $operator, $version2, $message = null, $propertyPath = null)
{
static::notEmpty($operator, 'versionCompare operator is required and cannot be empty.');
if (true !== \version_compare($version1, $version2, $operator)) {
$message = \sprintf(
static::generateMessage($message ?: 'Version "%s" is not "%s" version "%s".'),
static::stringify($version1),
static::stringify($operator),
static::stringify($version2)
);
throw static::createException($version1, $message, static::INVALID_VERSION, $propertyPath, ['operator' => $operator, 'version' => $version2]);
}
return true;
}
/**
* Assert on PHP version.
*
* @param string $operator
* @param mixed $version
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function phpVersion($operator, $version, $message = null, $propertyPath = null)
{
static::defined('PHP_VERSION');
return static::version(PHP_VERSION, $operator, $version, $message, $propertyPath);
}
/**
* Assert that extension is loaded and a specific version is installed.
*
* @param string $extension
* @param string $operator
* @param mixed $version
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function extensionVersion($extension, $operator, $version, $message = null, $propertyPath = null)
{
static::extensionLoaded($extension, $message, $propertyPath);
return static::version(\phpversion($extension), $operator, $version, $message, $propertyPath);
}
/**
* Determines that the provided value is callable.
*
* @param mixed $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function isCallable($value, $message = null, $propertyPath = null)
{
if (!\is_callable($value)) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is not a callable.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_CALLABLE, $propertyPath);
}
return true;
}
/**
* Assert that the provided value is valid according to a callback.
*
* If the callback returns `false` the assertion will fail.
*
* @param mixed $value
* @param callable $callback
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function satisfy($value, $callback, $message = null, $propertyPath = null)
{
static::isCallable($callback);
if (false === \call_user_func($callback, $value)) {
$message = \sprintf(
static::generateMessage($message ?: 'Provided "%s" is invalid according to custom rule.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_SATISFY, $propertyPath);
}
return true;
}
/**
* Assert that value is an IPv4 or IPv6 address
* (using input_filter/FILTER_VALIDATE_IP).
*
* @param string $value
* @param int|null $flag
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*
* @see http://php.net/manual/filter.filters.flags.php
*/
public static function ip($value, $flag = null, $message = null, $propertyPath = null)
{
static::string($value, $message, $propertyPath);
if (!\filter_var($value, FILTER_VALIDATE_IP, $flag)) {
$message = \sprintf(
static::generateMessage($message ?: 'Value "%s" was expected to be a valid IP address.'),
static::stringify($value)
);
throw static::createException($value, $message, static::INVALID_IP, $propertyPath, ['flag' => $flag]);
}
return true;
}
/**
* Assert that value is an IPv4 address
* (using input_filter/FILTER_VALIDATE_IP).
*
* @param string $value
* @param int|null $flag
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*
* @see http://php.net/manual/filter.filters.flags.php
*/
public static function ipv4($value, $flag = null, $message = null, $propertyPath = null)
{
static::ip($value, $flag | FILTER_FLAG_IPV4, static::generateMessage($message ?: 'Value "%s" was expected to be a valid IPv4 address.'), $propertyPath);
return true;
}
/**
* Assert that value is an IPv6 address
* (using input_filter/FILTER_VALIDATE_IP).
*
* @param string $value
* @param int|null $flag
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*
* @see http://php.net/manual/filter.filters.flags.php
*/
public static function ipv6($value, $flag = null, $message = null, $propertyPath = null)
{
static::ip($value, $flag | FILTER_FLAG_IPV6, static::generateMessage($message ?: 'Value "%s" was expected to be a valid IPv6 address.'), $propertyPath);
return true;
}
/**
* Assert that a constant is defined.
*
* @param mixed $constant
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function defined($constant, $message = null, $propertyPath = null)
{
if (!\defined($constant)) {
$message = \sprintf(static::generateMessage($message ?: 'Value "%s" expected to be a defined constant.'), $constant);
throw static::createException($constant, $message, static::INVALID_CONSTANT, $propertyPath);
}
return true;
}
/**
* Assert that a constant is defined.
*
* @param string $value
* @param string|callable|null $message
* @param string|null $propertyPath
*
* @return bool
*/
public static function base64($value, $message = null, $propertyPath = null)
{
if (false === \base64_decode($value, true)) {
$message = \sprintf(static::generateMessage($message ?: 'Value "%s" is not a valid base64 string.'), $value);
throw static::createException($value, $message, static::INVALID_BASE64, $propertyPath);
}
return true;
}
/**
* Helper method that handles building the assertion failure exceptions.
* They are returned from this method so that the stack trace still shows
* the assertions method.
*
* @param mixed $value
* @param string|callable $message
* @param int $code
* @param string|null $propertyPath
* @param array $constraints
*
* @return mixed
*/
protected static function createException($value, $message, $code, $propertyPath = null, array $constraints = [])
{
$exceptionClass = static::$exceptionClass;
return new $exceptionClass($message, $code, $propertyPath, $value, $constraints);
}
/**
* Make a string version of a value.
*
* @param mixed $value
*
* @return string
*/
protected static function stringify($value)
{
$result = \gettype($value);
if (\is_bool($value)) {
$result = $value ? '' : '';
} elseif (\is_scalar($value)) {
$val = (string) $value;
if (\strlen($val) > 100) {
$val = \substr($val, 0, 97).'...';
}
$result = $val;
} elseif (\is_array($value)) {
$result = '';
} elseif (\is_object($value)) {
$result = \get_class($value);
} elseif (\is_resource($value)) {
$result = \get_resource_type($value);
} elseif (null === $value) {
$result = '';
}
return $result;
}
/**
* Generate the message.
*
* @param string|callable $message
*
* @return string
*/
protected static function generateMessage($message): string
{
if (\is_callable($message)) {
$traces = \debug_backtrace(0);
$parameters = [];
try {
$reflection = new \ReflectionClass($traces[1]['class']);
$method = $reflection->getMethod($traces[1]['function']);
foreach ($method->getParameters() as $index => $parameter) {
if ('message' !== $parameter->getName()) {
$parameters[$parameter->getName()] = \array_key_exists($index, $traces[1]['args'])
? $traces[1]['args'][$index]
: $parameter->getDefaultValue();
}
}
$parameters['::assertion'] = \sprintf('%s%s%s', $traces[1]['class'], $traces[1]['type'], $traces[1]['function']);
$message = \call_user_func_array($message, [$parameters]);
} // @codeCoverageIgnoreStart
catch (\Throwable $exception) {
$message = \sprintf('Unable to generate message : %s', $exception->getMessage());
} // @codeCoverageIgnoreEnd
}
return (string) $message;
}
}