ss_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
// .
PRESS_PRIVACY::IS_PUBLIC ), $videopress_privacy_setting_list, true ) ) {
$videopress_privacy_setting_list[] = \VIDEOPRESS_PRIVACY::SITE_DEFAULT;
}
}
$args['meta_query'][] = array(
'key' => 'videopress_privacy_setting',
'value' => $videopress_privacy_setting_list,
'compare' => 'IN',
);
}
/* Filter using rating meta key */
if ( isset( $request['videopress_rating'] ) ) {
$videopress_rating = sanitize_text_field( $request['videopress_rating'] );
/* Allows the filtering to happens using a list of ratings separated by comma */
$videopress_rating_list = explode( ',', $videopress_rating );
$args['meta_query'][] = array(
'key' => 'videopress_rating',
'value' => $videopress_rating_list,
'compare' => 'IN',
);
}
return $args;
}
/**
* Defines data structure and what elements are visible in which contexts
*/
public function get_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->field_name,
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'description' => __( 'VideoPress Data', 'jetpack-videopress-pkg' ),
);
}
/**
* Getter: Retrieve current VideoPress data for a given attachment.
*
* @param array $attachment Response from the attachment endpoint.
* @param WP_REST_Request $request Request to the attachment endpoint.
*
* @return array
*/
public function get( $attachment, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! isset( $attachment['id'] ) ) {
return array();
}
$blog_id = Jetpack_Connection::get_site_id();
if ( ! is_int( $blog_id ) ) {
return array();
}
$videopress = $this->get_videopress_data( (int) $attachment['id'], $blog_id );
if ( ! $videopress ) {
return array();
}
return $videopress;
}
/**
* Gets the VideoPress GUID for a given attachment.
*
* This is pulled out into a separate method to support unit test mocking.
*
* @param int $attachment_id Attachment ID.
* @param int $blog_id Blog ID.
*
* @return array
*/
public function get_videopress_data( $attachment_id, $blog_id ) {
$info = video_get_info_by_blogpostid( $blog_id, $attachment_id );
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$title = video_get_title( $blog_id, $attachment_id );
$description = video_get_description( $blog_id, $attachment_id );
$video_attachment = get_blog_post( $blog_id, $attachment_id );
if ( null === $video_attachment ) {
$caption = '';
} else {
$caption = $video_attachment->post_excerpt;
}
} else {
$title = $info->title;
$description = $info->description;
$caption = $info->caption;
}
$video_privacy_setting = ! isset( $info->privacy_setting ) ? \VIDEOPRESS_PRIVACY::SITE_DEFAULT : intval( $info->privacy_setting );
$private_enabled_for_site = Data::get_videopress_videos_private_for_site();
$is_private = $this->video_is_private( $video_privacy_setting, $private_enabled_for_site );
// The video needs a playback token if it's private for any reason (video privacy setting or site default privacy setting)
$video_needs_playback_token = $is_private;
return array(
'title' => $title,
'description' => $description,
'caption' => $caption,
'guid' => $info->guid ?? null,
'rating' => $info->rating ?? null,
'allow_download' =>
isset( $info->allow_download ) && $info->allow_download ? 1 : 0,
'display_embed' =>
isset( $info->display_embed ) && $info->display_embed ? 1 : 0,
'privacy_setting' => $video_privacy_setting,
'needs_playback_token' => $video_needs_playback_token,
'is_private' => $is_private,
'private_enabled_for_site' => $private_enabled_for_site,
);
}
/**
* Checks if the given attachment is a video.
*
* @param object $attachment The attachment object.
*
* @return false|int
*/
public function is_video( $attachment ) {
return isset( $attachment->post_mime_type ) && wp_startswith( $attachment->post_mime_type, 'video/' );
}
/**
* Removes the jetpack_videopress field from the response if the
* given attachment is not a video.
*
* @param WP_REST_Response $response Response from the attachment endpoint.
* @param WP_Post $attachment The original attachment object.
*
* @return mixed
*/
public function remove_field_for_non_videos( $response, $attachment ) {
if ( ! $this->is_video( $attachment ) ) {
unset( $response->data[ $this->field_name ] );
}
return $response;
}
/**
* Determines if a video is private based on the video privacy
* setting and the site default privacy setting.
*
* @param int $video_privacy_setting The privacy setting for the video.
* @param bool $private_enabled_for_site Flag stating if the default video privacy is private.
*
* @return bool
*/
private function video_is_private( $video_privacy_setting, $private_enabled_for_site ) {
if ( $video_privacy_setting === \VIDEOPRESS_PRIVACY::IS_PUBLIC ) {
return false;
}
if ( $video_privacy_setting === \VIDEOPRESS_PRIVACY::IS_PRIVATE ) {
return true;
}
return $private_enabled_for_site;
}
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
wpcom_rest_api_v2_load_plugin( 'Automattic\Jetpack\VideoPress\WPCOM_REST_API_V2_Attachment_VideoPress_Data' );
}