video = null; $default_types = wp_get_video_extensions(); $defaults_atts = array( 'src' => '', 'poster' => '', 'loop' => '', 'autoplay' => '', 'muted' => 'false', 'preload' => 'metadata', 'width' => 640, 'height' => 360, 'class' => 'wp-video-shortcode', ); foreach ( $default_types as $type ) { $defaults_atts[ $type ] = ''; } $atts = shortcode_atts( $defaults_atts, $attr, 'video' ); if ( is_admin() ) { // Shrink the video so it isn't huge in the admin. if ( $atts['width'] > $defaults_atts['width'] ) { $atts['height'] = round( ( $atts['height'] * $defaults_atts['width'] ) / $atts['width'] ); $atts['width'] = $defaults_atts['width']; } } else { // If the video is bigger than the theme. if ( ! empty( $content_width ) && $atts['width'] > $content_width ) { $atts['height'] = round( ( $atts['height'] * $content_width ) / $atts['width'] ); $atts['width'] = $content_width; } } $is_vimeo = false; $is_youtube = false; $yt_pattern = '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#'; $vimeo_pattern = '#^https?://(.+\.)?vimeo\.com/.*#'; $primary = false; if ( ! empty( $atts['src'] ) ) { $is_vimeo = ( preg_match( $vimeo_pattern, $atts['src'] ) ); $is_youtube = ( preg_match( $yt_pattern, $atts['src'] ) ); if ( ! $is_youtube && ! $is_vimeo ) { $type = wp_check_filetype( $atts['src'], wp_get_mime_types() ); if ( ! in_array( strtolower( $type['ext'] ), $default_types, true ) ) { return sprintf( '%s', esc_url( $atts['src'] ), esc_html( $atts['src'] ) ); } } if ( $is_vimeo ) { wp_enqueue_script( 'mediaelement-vimeo' ); } $primary = true; array_unshift( $default_types, 'src' ); } else { foreach ( $default_types as $ext ) { if ( ! empty( $atts[ $ext ] ) ) { $type = wp_check_filetype( $atts[ $ext ], wp_get_mime_types() ); if ( strtolower( $type['ext'] ) === $ext ) { $primary = true; } } } } if ( ! $primary ) { $videos = get_attached_media( 'video', $post_id ); if ( empty( $videos ) ) { return; } $video = reset( $videos ); $atts['src'] = wp_get_attachment_url( $video->ID ); if ( empty( $atts['src'] ) ) { return; } array_unshift( $default_types, 'src' ); } /** * Filters the media library used for the video shortcode. * * @since 3.6.0 * * @param string $library Media library used for the video shortcode. */ $library = apply_filters( 'wp_video_shortcode_library', 'mediaelement' ); if ( 'mediaelement' === $library && did_action( 'init' ) ) { wp_enqueue_style( 'wp-mediaelement' ); wp_enqueue_script( 'wp-mediaelement' ); wp_enqueue_script( 'mediaelement-vimeo' ); } /* * MediaElement.js has issues with some URL formats for Vimeo and YouTube, * so update the URL to prevent the ME.js player from breaking. */ if ( 'mediaelement' === $library ) { if ( $is_youtube ) { // Remove `feature` query arg and force SSL - see #40866. $atts['src'] = remove_query_arg( 'feature', $atts['src'] ); $atts['src'] = set_url_scheme( $atts['src'], 'https' ); } elseif ( $is_vimeo ) { // Remove all query arguments and force SSL - see #40866. $parsed_vimeo_url = wp_parse_url( $atts['src'] ); $vimeo_src = 'https://' . $parsed_vimeo_url['host'] . $parsed_vimeo_url['path']; // Add loop param for mejs bug - see #40977, not needed after #39686. $loop = $atts['loop'] ? '1' : '0'; $atts['src'] = add_query_arg( 'loop', $loop, $vimeo_src ); } } /** * Filters the class attribute for the video shortcode output container. * * @since 3.6.0 * @since 4.9.0 The `$atts` parameter was added. * * @param string $class CSS class or list of space-separated classes. * @param array $atts Array of video shortcode attributes. */ $atts['class'] = apply_filters( 'wp_video_shortcode_class', $atts['class'], $atts ); $html_atts = array( 'class' => $atts['class'], 'id' => sprintf( 'video-%d-%d', $post_id, $instance ), 'width' => absint( $atts['width'] ), 'height' => absint( $atts['height'] ), 'poster' => esc_url( $atts['poster'] ), 'loop' => wp_validate_boolean( $atts['loop'] ), 'autoplay' => wp_validate_boolean( $atts['autoplay'] ), 'muted' => wp_validate_boolean( $atts['muted'] ), 'preload' => $atts['preload'], ); // These ones should just be omitted altogether if they are blank. foreach ( array( 'poster', 'loop', 'autoplay', 'preload', 'muted' ) as $a ) { if ( empty( $html_atts[ $a ] ) ) { unset( $html_atts[ $a ] ); } } $attr_strings = array(); foreach ( $html_atts as $k => $v ) { $attr_strings[] = $k . '="' . esc_attr( $v ) . '"'; } $html = ''; if ( 'mediaelement' === $library && 1 === $instance ) { $html .= "\n"; } $html .= sprintf( ''; $width_rule = ''; if ( ! empty( $atts['width'] ) ) { $width_rule = sprintf( 'width: %dpx;', $atts['width'] ); } $output = sprintf( '
%s
', $width_rule, $html ); /** * Filters the output of the video shortcode. * * @since 3.6.0 * * @param string $output Video shortcode HTML output. * @param array $atts Array of video shortcode attributes. * @param string $video Video file. * @param int $post_id Post ID. * @param string $library Media library used for the video shortcode. */ return apply_filters( 'wp_video_shortcode', $output, $atts, $video, $post_id, $library ); } add_shortcode( 'video', 'wp_video_shortcode' ); /** * Gets the previous image link that has the same post parent. * * @since 5.8.0 * * @see get_adjacent_image_link() * * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array * of width and height values in pixels (in that order). Default 'thumbnail'. * @param string|false $text Optional. Link text. Default false. * @return string Markup for previous image link. */ function get_previous_image_link( $size = 'thumbnail', $text = false ) { return get_adjacent_image_link( true, $size, $text ); } /** * Displays previous image link that has the same post parent. * * @since 2.5.0 * * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array * of width and height values in pixels (in that order). Default 'thumbnail'. * @param string|false $text Optional. Link text. Default false. */ function previous_image_link( $size = 'thumbnail', $text = false ) { echo get_previous_image_link( $size, $text ); } /** * Gets the next image link that has the same post parent. * * @since 5.8.0 * * @see get_adjacent_image_link() * * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array * of width and height values in pixels (in that order). Default 'thumbnail'. * @param string|false $text Optional. Link text. Default false. * @return string Markup for next image link. */ function get_next_image_link( $size = 'thumbnail', $text = false ) { return get_adjacent_image_link( false, $size, $text ); } /** * Displays next image link that has the same post parent. * * @since 2.5.0 * * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array * of width and height values in pixels (in that order). Default 'thumbnail'. * @param string|false $text Optional. Link text. Default false. */ function next_image_link( $size = 'thumbnail', $text = false ) { echo get_next_image_link( $size, $text ); } /** * Gets the next or previous image link that has the same post parent. * * Retrieves the current attachment object from the $post global. * * @since 5.8.0 * * @param bool $prev Optional. Whether to display the next (false) or previous (true) link. Default true. * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array * of width and height values in pixels (in that order). Default 'thumbnail'. * @param bool $text Optional. Link text. Default false. * @return string Markup for image link. */ function get_adjacent_image_link( $prev = true, $size = 'thumbnail', $text = false ) { $post = get_post(); $attachments = array_values( get_children( array( 'post_parent' => $post->post_parent, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => 'ASC', 'orderby' => 'menu_order ID', ) ) ); foreach ( $attachments as $k => $attachment ) { if ( (int) $attachment->ID === (int) $post->ID ) { break; } } $output = ''; $attachment_id = 0; if ( $attachments ) { $k = $prev ? $k - 1 : $k + 1; if ( isset( $attachments[ $k ] ) ) { $attachment_id = $attachments[ $k ]->ID; $attr = array( 'alt' => get_the_title( $attachment_id ) ); $output = wp_get_attachment_link( $attachment_id, $size, true, false, $text, $attr ); } } $adjacent = $prev ? 'previous' : 'next'; /** * Filters the adjacent image link. * * The dynamic portion of the hook name, `$adjacent`, refers to the type of adjacency, * either 'next', or 'previous'. * * Possible hook names include: * * - `next_image_link` * - `previous_image_link` * * @since 3.5.0 * * @param string $output Adjacent image HTML markup. * @param int $attachment_id Attachment ID * @param string|int[] $size Requested image size. Can be any registered image size name, or * an array of width and height values in pixels (in that order). * @param string $text Link text. */ return apply_filters( "{$adjacent}_image_link", $output, $attachment_id, $size, $text ); } /** * Displays next or previous image link that has the same post parent. * * Retrieves the current attachment object from the $post global. * * @since 2.5.0 * * @param bool $prev Optional. Whether to display the next (false) or previous (true) link. Default true. * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array * of width and height values in pixels (in that order). Default 'thumbnail'. * @param bool $text Optional. Link text. Default false. */ function adjacent_image_link( $prev = true, $size = 'thumbnail', $text = false ) { echo get_adjacent_image_link( $prev, $size, $text ); } /** * Retrieves taxonomies attached to given the attachment. * * @since 2.5.0 * @since 4.7.0 Introduced the `$output` parameter. * * @param int|array|object $attachment Attachment ID, data array, or data object. * @param string $output Output type. 'names' to return an array of taxonomy names, * or 'objects' to return an array of taxonomy objects. * Default is 'names'. * @return string[]|WP_Taxonomy[] List of taxonomies or taxonomy names. Empty array on failure. */ function get_attachment_taxonomies( $attachment, $output = 'names' ) { if ( is_int( $attachment ) ) { $attachment = get_post( $attachment ); } elseif ( is_array( $attachment ) ) { $attachment = (object) $attachment; } if ( ! is_object( $attachment ) ) { return array(); } $file = get_attached_file( $attachment->ID ); $filename = wp_basename( $file ); $objects = array( 'attachment' ); if ( str_contains( $filename, '.' ) ) { $objects[] = 'attachment:' . substr( $filename, strrpos( $filename, '.' ) + 1 ); } if ( ! empty( $attachment->post_mime_type ) ) { $objects[] = 'attachment:' . $attachment->post_mime_type; if ( str_contains( $attachment->post_mime_type, '/' ) ) { foreach ( explode( '/', $attachment->post_mime_type ) as $token ) { if ( ! empty( $token ) ) { $objects[] = "attachment:$token"; } } } } $taxonomies = array(); foreach ( $objects as $object ) { $taxes = get_object_taxonomies( $object, $output ); if ( $taxes ) { $taxonomies = array_merge( $taxonomies, $taxes ); } } if ( 'names' === $output ) { $taxonomies = array_unique( $taxonomies ); } return $taxonomies; } /** * Retrieves all of the taxonomies that are registered for attachments. * * Handles mime-type-specific taxonomies such as attachment:image and attachment:video. * * @since 3.5.0 * * @see get_taxonomies() * * @param string $output Optional. The type of taxonomy output to return. Accepts 'names' or 'objects'. * Default 'names'. * @return string[]|WP_Taxonomy[] Array of names or objects of registered taxonomies for attachments. */ function get_taxonomies_for_attachments( $output = 'names' ) { $taxonomies = array(); foreach ( get_taxonomies( array(), 'objects' ) as $taxonomy ) { foreach ( $taxonomy->object_type as $object_type ) { if ( 'attachment' === $object_type || str_starts_with( $object_type, 'attachment:' ) ) { if ( 'names' === $output ) { $taxonomies[] = $taxonomy->name; } else { $taxonomies[ $taxonomy->name ] = $taxonomy; } break; } } } return $taxonomies; } /** * Determines whether the value is an acceptable type for GD image functions. * * In PHP 8.0, the GD extension uses GdImage objects for its data structures. * This function checks if the passed value is either a GdImage object instance * or a resource of type `gd`. Any other type will return false. * * @since 5.6.0 * * @param resource|GdImage|false $image A value to check the type for. * @return bool True if `$image` is either a GD image resource or a GdImage instance, * false otherwise. */ function is_gd_image( $image ) { if ( $image instanceof GdImage || is_resource( $image ) && 'gd' === get_resource_type( $image ) ) { return true; } return false; } /** * Creates a new GD image resource with transparency support. * * @todo Deprecate if possible. * * @since 2.9.0 * * @param int $width Image width in pixels. * @param int $height Image height in pixels. * @return resource|GdImage|false The GD image resource or GdImage instance on success. * False on failure. */ function wp_imagecreatetruecolor( $width, $height ) { $img = imagecreatetruecolor( $width, $height ); if ( is_gd_image( $img ) && function_exists( 'imagealphablending' ) && function_exists( 'imagesavealpha' ) ) { imagealphablending( $img, false ); imagesavealpha( $img, true ); } return $img; } /** * Based on a supplied width/height example, returns the biggest possible dimensions based on the max width/height. * * @since 2.9.0 * * @see wp_constrain_dimensions() * * @param int $example_width The width of an example embed. * @param int $example_height The height of an example embed. * @param int $max_width The maximum allowed width. * @param int $max_height The maximum allowed height. * @return int[] { * An array of maximum width and height values. * * @type int $0 The maximum width in pixels. * @type int $1 The maximum height in pixels. * } */ function wp_expand_dimensions( $example_width, $example_height, $max_width, $max_height ) { $example_width = (int) $example_width; $example_height = (int) $example_height; $max_width = (int) $max_width; $max_height = (int) $max_height; return wp_constrain_dimensions( $example_width * 1000000, $example_height * 1000000, $max_width, $max_height ); } /** * Determines the maximum upload size allowed in php.ini. * * @since 2.5.0 * * @return int Allowed upload size. */ function wp_max_upload_size() { $u_bytes = wp_convert_hr_to_bytes( ini_get( 'upload_max_filesize' ) ); $p_bytes = wp_convert_hr_to_bytes( ini_get( 'post_max_size' ) ); /** * Filters the maximum upload size allowed in php.ini. * * @since 2.5.0 * * @param int $size Max upload size limit in bytes. * @param int $u_bytes Maximum upload filesize in bytes. * @param int $p_bytes Maximum size of POST data in bytes. */ return apply_filters( 'upload_size_limit', min( $u_bytes, $p_bytes ), $u_bytes, $p_bytes ); } /** * Returns a WP_Image_Editor instance and loads file into it. * * @since 3.5.0 * * @param string $path Path to the file to load. * @param array $args Optional. Additional arguments for retrieving the image editor. * Default empty array. * @return WP_Image_Editor|WP_Error The WP_Image_Editor object on success, * a WP_Error object otherwise. */ function wp_get_image_editor( $path, $args = array() ) { $args['path'] = $path; // If the mime type is not set in args, try to extract and set it from the file. if ( ! isset( $args['mime_type'] ) ) { $file_info = wp_check_filetype( $args['path'] ); /* * If $file_info['type'] is false, then we let the editor attempt to * figure out the file type, rather than forcing a failure based on extension. */ if ( isset( $file_info ) && $file_info['type'] ) { $args['mime_type'] = $file_info['type']; } } // Check and set the output mime type mapped to the input type. if ( isset( $args['mime_type'] ) ) { /** This filter is documented in wp-includes/class-wp-image-editor.php */ $output_format = apply_filters( 'image_editor_output_format', array(), $path, $args['mime_type'] ); if ( isset( $output_format[ $args['mime_type'] ] ) ) { $args['output_mime_type'] = $output_format[ $args['mime_type'] ]; } } $implementation = _wp_image_editor_choose( $args ); if ( $implementation ) { $editor = new $implementation( $path ); $loaded = $editor->load(); if ( is_wp_error( $loaded ) ) { return $loaded; } return $editor; } return new WP_Error( 'image_no_editor', __( 'No editor could be selected.' ) ); } /** * Tests whether there is an editor that supports a given mime type or methods. * * @since 3.5.0 * * @param string|array $args Optional. Array of arguments to retrieve the image editor supports. * Default empty array. * @return bool True if an eligible editor is found; false otherwise. */ function wp_image_editor_supports( $args = array() ) { return (bool) _wp_image_editor_choose( $args ); } /** * Tests which editors are capable of supporting the request. * * @ignore * @since 3.5.0 * * @param array $args Optional. Array of arguments for choosing a capable editor. Default empty array. * @return string|false Class name for the first editor that claims to support the request. * False if no editor claims to support the request. */ function _wp_image_editor_choose( $args = array() ) { require_once ABSPATH . WPINC . '/class-wp-image-editor.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php'; require_once ABSPATH . WPINC . '/class-avif-info.php'; /** * Filters the list of image editing library classes. * * @since 3.5.0 * * @param string[] $image_editors Array of available image editor class names. Defaults are * 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD'. */ $implementations = apply_filters( 'wp_image_editors', array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' ) ); $supports_input = false; foreach ( $implementations as $implementation ) { if ( ! call_user_func( array( $implementation, 'test' ), $args ) ) { continue; } // Implementation should support the passed mime type. if ( isset( $args['mime_type'] ) && ! call_user_func( array( $implementation, 'supports_mime_type' ), $args['mime_type'] ) ) { continue; } // Implementation should support requested methods. if ( isset( $args['methods'] ) && array_diff( $args['methods'], get_class_methods( $implementation ) ) ) { continue; } // Implementation should ideally support the output mime type as well if set and different than the passed type. if ( isset( $args['mime_type'] ) && isset( $args['output_mime_type'] ) && $args['mime_type'] !== $args['output_mime_type'] && ! call_user_func( array( $implementation, 'supports_mime_type' ), $args['output_mime_type'] ) ) { /* * This implementation supports the input type but not the output type. * Keep looking to see if we can find an implementation that supports both. */ $supports_input = $implementation; continue; } // Favor the implementation that supports both input and output mime types. return $implementation; } return $supports_input; } /** * Prints default Plupload arguments. * * @since 3.4.0 */ function wp_plupload_default_settings() { $wp_scripts = wp_scripts(); $data = $wp_scripts->get_data( 'wp-plupload', 'data' ); if ( $data && str_contains( $data, '_wpPluploadSettings' ) ) { return; } $max_upload_size = wp_max_upload_size(); $allowed_extensions = array_keys( get_allowed_mime_types() ); $extensions = array(); foreach ( $allowed_extensions as $extension ) { $extensions = array_merge( $extensions, explode( '|', $extension ) ); } /* * Since 4.9 the `runtimes` setting is hardcoded in our version of Plupload to `html5,html4`, * and the `flash_swf_url` and `silverlight_xap_url` are not used. */ $defaults = array( 'file_data_name' => 'async-upload', // Key passed to $_FILE. 'url' => admin_url( 'async-upload.php', 'relative' ), 'filters' => array( 'max_file_size' => $max_upload_size . 'b', 'mime_types' => array( array( 'extensions' => implode( ',', $extensions ) ) ), ), ); /* * Currently only iOS Safari supports multiple files uploading, * but iOS 7.x has a bug that prevents uploading of videos when enabled. * See #29602. */ if ( wp_is_mobile() && str_contains( $_SERVER['HTTP_USER_AGENT'], 'OS 7_' ) && str_contains( $_SERVER['HTTP_USER_AGENT'], 'like Mac OS X' ) ) { $defaults['multi_selection'] = false; } // Check if WebP images can be edited. if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) { $defaults['webp_upload_error'] = true; } // Check if AVIF images can be edited. if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) { $defaults['avif_upload_error'] = true; } /** * Filters the Plupload default settings. * * @since 3.4.0 * * @param array $defaults Default Plupload settings array. */ $defaults = apply_filters( 'plupload_default_settings', $defaults ); $params = array( 'action' => 'upload-attachment', ); /** * Filters the Plupload default parameters. * * @since 3.4.0 * * @param array $params Default Plupload parameters array. */ $params = apply_filters( 'plupload_default_params', $params ); $params['_wpnonce'] = wp_create_nonce( 'media-form' ); $defaults['multipart_params'] = $params; $settings = array( 'defaults' => $defaults, 'browser' => array( 'mobile' => wp_is_mobile(), 'supported' => _device_can_upload(), ), 'limitExceeded' => is_multisite() && ! is_upload_space_available(), ); $script = 'var _wpPluploadSettings = ' . wp_json_encode( $settings ) . ';'; if ( $data ) { $script = "$data\n$script"; } $wp_scripts->add_data( 'wp-plupload', 'data', $script ); } /** * Prepares an attachment post object for JS, where it is expected * to be JSON-encoded and fit into an Attachment model. * * @since 3.5.0 * * @param int|WP_Post $attachment Attachment ID or object. * @return array|void { * Array of attachment details, or void if the parameter does not correspond to an attachment. * * @type string $alt Alt text of the attachment. * @type string $author ID of the attachment author, as a string. * @type string $authorName Name of the attachment author. * @type string $caption Caption for the attachment. * @type array $compat Containing item and meta. * @type string $context Context, whether it's used as the site icon for example. * @type int $date Uploaded date, timestamp in milliseconds. * @type string $dateFormatted Formatted date (e.g. June 29, 2018). * @type string $description Description of the attachment. * @type string $editLink URL to the edit page for the attachment. * @type string $filename File name of the attachment. * @type string $filesizeHumanReadable Filesize of the attachment in human readable format (e.g. 1 MB). * @type int $filesizeInBytes Filesize of the attachment in bytes. * @type int $height If the attachment is an image, represents the height of the image in pixels. * @type string $icon Icon URL of the attachment (e.g. /wp-includes/images/media/archive.png). * @type int $id ID of the attachment. * @type string $link URL to the attachment. * @type int $menuOrder Menu order of the attachment post. * @type array $meta Meta data for the attachment. * @type string $mime Mime type of the attachment (e.g. image/jpeg or application/zip). * @type int $modified Last modified, timestamp in milliseconds. * @type string $name Name, same as title of the attachment. * @type array $nonces Nonces for update, delete and edit. * @type string $orientation If the attachment is an image, represents the image orientation * (landscape or portrait). * @type array $sizes If the attachment is an image, contains an array of arrays * for the images sizes: thumbnail, medium, large, and full. * @type string $status Post status of the attachment (usually 'inherit'). * @type string $subtype Mime subtype of the attachment (usually the last part, e.g. jpeg or zip). * @type string $title Title of the attachment (usually slugified file name without the extension). * @type string $type Type of the attachment (usually first part of the mime type, e.g. image). * @type int $uploadedTo Parent post to which the attachment was uploaded. * @type string $uploadedToLink URL to the edit page of the parent post of the attachment. * @type string $uploadedToTitle Post title of the parent of the attachment. * @type string $url Direct URL to the attachment file (from wp-content). * @type int $width If the attachment is an image, represents the width of the image in pixels. * } * */ function wp_prepare_attachment_for_js( $attachment ) { $attachment = get_post( $attachment ); if ( ! $attachment ) { return; } if ( 'attachment' !== $attachment->post_type ) { return; } $meta = wp_get_attachment_metadata( $attachment->ID ); if ( str_contains( $attachment->post_mime_type, '/' ) ) { list( $type, $subtype ) = explode( '/', $attachment->post_mime_type ); } else { list( $type, $subtype ) = array( $attachment->post_mime_type, '' ); } $attachment_url = wp_get_attachment_url( $attachment->ID ); $base_url = str_replace( wp_basename( $attachment_url ), '', $attachment_url ); $response = array( 'id' => $attachment->ID, 'title' => $attachment->post_title, 'filename' => wp_basename( get_attached_file( $attachment->ID ) ), 'url' => $attachment_url, 'link' => get_attachment_link( $attachment->ID ), 'alt' => get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ), 'author' => $attachment->post_author, 'description' => $attachment->post_content, 'caption' => $attachment->post_excerpt, 'name' => $attachment->post_name, 'status' => $attachment->post_status, 'uploadedTo' => $attachment->post_parent, 'date' => strtotime( $attachment->post_date_gmt ) * 1000, 'modified' => strtotime( $attachment->post_modified_gmt ) * 1000, 'menuOrder' => $attachment->menu_order, 'mime' => $attachment->post_mime_type, 'type' => $type, 'subtype' => $subtype, 'icon' => wp_mime_type_icon( $attachment->ID, '.svg' ), 'dateFormatted' => mysql2date( __( 'F j, Y' ), $attachment->post_date ), 'nonces' => array( 'update' => false, 'delete' => false, 'edit' => false, ), 'editLink' => false, 'meta' => false, ); $author = new WP_User( $attachment->post_author ); if ( $author->exists() ) { $author_name = $author->display_name ? $author->display_name : $author->nickname; $response['authorName'] = html_entity_decode( $author_name, ENT_QUOTES, get_bloginfo( 'charset' ) ); $response['authorLink'] = get_edit_user_link( $author->ID ); } else { $response['authorName'] = __( '(no author)' ); } if ( $attachment->post_parent ) { $post_parent = get_post( $attachment->post_parent ); if ( $post_parent ) { $response['uploadedToTitle'] = $post_parent->post_title ? $post_parent->post_title : __( '(no title)' ); $response['uploadedToLink'] = get_edit_post_link( $attachment->post_parent, 'raw' ); } } $attached_file = get_attached_file( $attachment->ID ); if ( isset( $meta['filesize'] ) ) { $bytes = $meta['filesize']; } elseif ( file_exists( $attached_file ) ) { $bytes = wp_filesize( $attached_file ); } else { $bytes = ''; } if ( $bytes ) { $response['filesizeInBytes'] = $bytes; $response['filesizeHumanReadable'] = size_format( $bytes ); } $context = get_post_meta( $attachment->ID, '_wp_attachment_context', true ); $response['context'] = ( $context ) ? $context : ''; if ( current_user_can( 'edit_post', $attachment->ID ) ) { $response['nonces']['update'] = wp_create_nonce( 'update-post_' . $attachment->ID ); $response['nonces']['edit'] = wp_create_nonce( 'image_editor-' . $attachment->ID ); $response['editLink'] = get_edit_post_link( $attachment->ID, 'raw' ); } if ( current_user_can( 'delete_post', $attachment->ID ) ) { $response['nonces']['delete'] = wp_create_nonce( 'delete-post_' . $attachment->ID ); } if ( $meta && ( 'image' === $type || ! empty( $meta['sizes'] ) ) ) { $sizes = array(); /** This filter is documented in wp-admin/includes/media.php */ $possible_sizes = apply_filters( 'image_size_names_choose', array( 'thumbnail' => __( 'Thumbnail' ), 'medium' => __( 'Medium' ), 'large' => __( 'Large' ), 'full' => __( 'Full Size' ), ) ); unset( $possible_sizes['full'] ); /* * Loop through all potential sizes that may be chosen. Try to do this with some efficiency. * First: run the image_downsize filter. If it returns something, we can use its data. * If the filter does not return something, then image_downsize() is just an expensive way * to check the image metadata, which we do second. */ foreach ( $possible_sizes as $size => $label ) { /** This filter is documented in wp-includes/media.php */ $downsize = apply_filters( 'image_downsize', false, $attachment->ID, $size ); if ( $downsize ) { if ( empty( $downsize[3] ) ) { continue; } $sizes[ $size ] = array( 'height' => $downsize[2], 'width' => $downsize[1], 'url' => $downsize[0], 'orientation' => $downsize[2] > $downsize[1] ? 'portrait' : 'landscape', ); } elseif ( isset( $meta['sizes'][ $size ] ) ) { // Nothing from the filter, so consult image metadata if we have it. $size_meta = $meta['sizes'][ $size ]; /* * We have the actual image size, but might need to further constrain it if content_width is narrower. * Thumbnail, medium, and full sizes are also checked against the site's height/width options. */ list( $width, $height ) = image_constrain_size_for_editor( $size_meta['width'], $size_meta['height'], $size, 'edit' ); $sizes[ $size ] = array( 'height' => $height, 'width' => $width, 'url' => $base_url . $size_meta['file'], 'orientation' => $height > $width ? 'portrait' : 'landscape', ); } } if ( 'image' === $type ) { if ( ! empty( $meta['original_image'] ) ) { $response['originalImageURL'] = wp_get_original_image_url( $attachment->ID ); $response['originalImageName'] = wp_basename( wp_get_original_image_path( $attachment->ID ) ); } $sizes['full'] = array( 'url' => $attachment_url ); if ( isset( $meta['height'], $meta['width'] ) ) { $sizes['full']['height'] = $meta['height']; $sizes['full']['width'] = $meta['width']; $sizes['full']['orientation'] = $meta['height'] > $meta['width'] ? 'portrait' : 'landscape'; } $response = array_merge( $response, $sizes['full'] ); } elseif ( $meta['sizes']['full']['file'] ) { $sizes['full'] = array( 'url' => $base_url . $meta['sizes']['full']['file'], 'height' => $meta['sizes']['full']['height'], 'width' => $meta['sizes']['full']['width'], 'orientation' => $meta['sizes']['full']['height'] > $meta['sizes']['full']['width'] ? 'portrait' : 'landscape', ); } $response = array_merge( $response, array( 'sizes' => $sizes ) ); } if ( $meta && 'video' === $type ) { if ( isset( $meta['width'] ) ) { $response['width'] = (int) $meta['width']; } if ( isset( $meta['height'] ) ) { $response['height'] = (int) $meta['height']; } } if ( $meta && ( 'audio' === $type || 'video' === $type ) ) { if ( isset( $meta['length_formatted'] ) ) { $response['fileLength'] = $meta['length_formatted']; $response['fileLengthHumanReadable'] = human_readable_duration( $meta['length_formatted'] ); } $response['meta'] = array(); foreach ( wp_get_attachment_id3_keys( $attachment, 'js' ) as $key => $label ) { $response['meta'][ $key ] = false; if ( ! empty( $meta[ $key ] ) ) { $response['meta'][ $key ] = $meta[ $key ]; } } $id = get_post_thumbnail_id( $attachment->ID ); if ( ! empty( $id ) ) { list( $src, $width, $height ) = wp_get_attachment_image_src( $id, 'full' ); $response['image'] = compact( 'src', 'width', 'height' ); list( $src, $width, $height ) = wp_get_attachment_image_src( $id, 'thumbnail' ); $response['thumb'] = compact( 'src', 'width', 'height' ); } else { $src = wp_mime_type_icon( $attachment->ID, '.svg' ); $width = 48; $height = 64; $response['image'] = compact( 'src', 'width', 'height' ); $response['thumb'] = compact( 'src', 'width', 'height' ); } } if ( function_exists( 'get_compat_media_markup' ) ) { $response['compat'] = get_compat_media_markup( $attachment->ID, array( 'in_modal' => true ) ); } if ( function_exists( 'get_media_states' ) ) { $media_states = get_media_states( $attachment ); if ( ! empty( $media_states ) ) { $response['mediaStates'] = implode( ', ', $media_states ); } } /** * Filters the attachment data prepared for JavaScript. * * @since 3.5.0 * * @param array $response Array of prepared attachment data. See {@see wp_prepare_attachment_for_js()}. * @param WP_Post $attachment Attachment object. * @param array|false $meta Array of attachment meta data, or false if there is none. */ return apply_filters( 'wp_prepare_attachment_for_js', $response, $attachment, $meta ); } /** * Enqueues all scripts, styles, settings, and templates necessary to use * all media JS APIs. * * @since 3.5.0 * * @global int $content_width * @global wpdb $wpdb WordPress database abstraction object. * @global WP_Locale $wp_locale WordPress date and time locale object. * * @param array $args { * Arguments for enqueuing media scripts. * * @type int|WP_Post $post Post ID or post object. * } */ function wp_enqueue_media( $args = array() ) { // Enqueue me just once per page, please. if ( did_action( 'wp_enqueue_media' ) ) { return; } global $content_width, $wpdb, $wp_locale; $defaults = array( 'post' => null, ); $args = wp_parse_args( $args, $defaults ); /* * We're going to pass the old thickbox media tabs to `media_upload_tabs` * to ensure plugins will work. We will then unset those tabs. */ $tabs = array( // handler action suffix => tab label 'type' => '', 'type_url' => '', 'gallery' => '', 'library' => '', ); /** This filter is documented in wp-admin/includes/media.php */ $tabs = apply_filters( 'media_upload_tabs', $tabs ); unset( $tabs['type'], $tabs['type_url'], $tabs['gallery'], $tabs['library'] ); $props = array( 'link' => get_option( 'image_default_link_type' ), // DB default is 'file'. 'align' => get_option( 'image_default_align' ), // Empty default. 'size' => get_option( 'image_default_size' ), // Empty default. ); $exts = array_merge( wp_get_audio_extensions(), wp_get_video_extensions() ); $mimes = get_allowed_mime_types(); $ext_mimes = array(); foreach ( $exts as $ext ) { foreach ( $mimes as $ext_preg => $mime_match ) { if ( preg_match( '#' . $ext . '#i', $ext_preg ) ) { $ext_mimes[ $ext ] = $mime_match; break; } } } /** * Allows showing or hiding the "Create Audio Playlist" button in the media library. * * By default, the "Create Audio Playlist" button will always be shown in * the media library. If this filter returns `null`, a query will be run * to determine whether the media library contains any audio items. This * was the default behavior prior to version 4.8.0, but this query is * expensive for large media libraries. * * @since 4.7.4 * @since 4.8.0 The filter's default value is `true` rather than `null`. * * @link https://core.trac.wordpress.org/ticket/31071 * * @param bool|null $show Whether to show the button, or `null` to decide based * on whether any audio files exist in the media library. */ $show_audio_playlist = apply_filters( 'media_library_show_audio_playlist', true ); if ( null === $show_audio_playlist ) { $show_audio_playlist = $wpdb->get_var( "SELECT ID FROM $wpdb->posts WHERE post_type = 'attachment' AND post_mime_type LIKE 'audio%' LIMIT 1" ); } /** * Allows showing or hiding the "Create Video Playlist" button in the media library. * * By default, the "Create Video Playlist" button will always be shown in * the media library. If this filter returns `null`, a query will be run * to determine whether the media library contains any video items. This * was the default behavior prior to version 4.8.0, but this query is * expensive for large media libraries. * * @since 4.7.4 * @since 4.8.0 The filter's default value is `true` rather than `null`. * * @link https://core.trac.wordpress.org/ticket/31071 * * @param bool|null $show Whether to show the button, or `null` to decide based * on whether any video files exist in the media library. */ $show_video_playlist = apply_filters( 'media_library_show_video_playlist', true ); if ( null === $show_video_playlist ) { $show_video_playlist = $wpdb->get_var( "SELECT ID FROM $wpdb->posts WHERE post_type = 'attachment' AND post_mime_type LIKE 'video%' LIMIT 1" ); } /** * Allows overriding the list of months displayed in the media library. * * By default (if this filter does not return an array), a query will be * run to determine the months that have media items. This query can be * expensive for large media libraries, so it may be desirable for sites to * override this behavior. * * @since 4.7.4 * * @link https://core.trac.wordpress.org/ticket/31071 * * @param stdClass[]|null $months An array of objects with `month` and `year` * properties, or `null` for default behavior. */ $months = apply_filters( 'media_library_months_with_files', null ); if ( ! is_array( $months ) ) { $months = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month FROM $wpdb->posts WHERE post_type = %s ORDER BY post_date DESC", 'attachment' ) ); } foreach ( $months as $month_year ) { $month_year->text = sprintf( /* translators: 1: Month, 2: Year. */ __( '%1$s %2$d' ), $wp_locale->get_month( $month_year->month ), $month_year->year ); } /** * Filters whether the Media Library grid has infinite scrolling. Default `false`. * * @since 5.8.0 * * @param bool $infinite Whether the Media Library grid has infinite scrolling. */ $infinite_scrolling = apply_filters( 'media_library_infinite_scrolling', false ); $settings = array( 'tabs' => $tabs, 'tabUrl' => add_query_arg( array( 'chromeless' => true ), admin_url( 'media-upload.php' ) ), 'mimeTypes' => wp_list_pluck( get_post_mime_types(), 0 ), /** This filter is documented in wp-admin/includes/media.php */ 'captions' => ! apply_filters( 'disable_captions', '' ), 'nonce' => array( 'sendToEditor' => wp_create_nonce( 'media-send-to-editor' ), 'setAttachmentThumbnail' => wp_create_nonce( 'set-attachment-thumbnail' ), ), 'post' => array( 'id' => 0, ), 'defaultProps' => $props, 'attachmentCounts' => array( 'audio' => ( $show_audio_playlist ) ? 1 : 0, 'video' => ( $show_video_playlist ) ? 1 : 0, ), 'oEmbedProxyUrl' => rest_url( 'oembed/1.0/proxy' ), 'embedExts' => $exts, 'embedMimes' => $ext_mimes, 'contentWidth' => $content_width, 'months' => $months, 'mediaTrash' => MEDIA_TRASH ? 1 : 0, 'infiniteScrolling' => ( $infinite_scrolling ) ? 1 : 0, ); $post = null; if ( isset( $args['post'] ) ) { $post = get_post( $args['post'] ); $settings['post'] = array( 'id' => $post->ID, 'nonce' => wp_create_nonce( 'update-post_' . $post->ID ), ); $thumbnail_support = current_theme_supports( 'post-thumbnails', $post->post_type ) && post_type_supports( $post->post_type, 'thumbnail' ); if ( ! $thumbnail_support && 'attachment' === $post->post_type && $post->post_mime_type ) { if ( wp_attachment_is( 'audio', $post ) ) { $thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' ); } elseif ( wp_attachment_is( 'video', $post ) ) { $thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' ); } } if ( $thumbnail_support ) { $featured_image_id = get_post_meta( $post->ID, '_thumbnail_id', true ); $settings['post']['featuredImageId'] = $featured_image_id ? $featured_image_id : -1; } } if ( $post ) { $post_type_object = get_post_type_object( $post->post_type ); } else { $post_type_object = get_post_type_object( 'post' ); } $strings = array( // Generic. 'mediaFrameDefaultTitle' => __( 'Media' ), 'url' => __( 'URL' ), 'addMedia' => __( 'Add media' ), 'search' => __( 'Search' ), 'select' => __( 'Select' ), 'cancel' => __( 'Cancel' ), 'update' => __( 'Update' ), 'replace' => __( 'Replace' ), 'remove' => __( 'Remove' ), 'back' => __( 'Back' ), /* * translators: This is a would-be plural string used in the media manager. * If there is not a word you can use in your language to avoid issues with the * lack of plural support here, turn it into "selected: %d" then translate it. */ 'selected' => __( '%d selected' ), 'dragInfo' => __( 'Drag and drop to reorder media files.' ), // Upload. 'uploadFilesTitle' => __( 'Upload files' ), 'uploadImagesTitle' => __( 'Upload images' ), // Library. 'mediaLibraryTitle' => __( 'Media Library' ), 'insertMediaTitle' => __( 'Add media' ), 'createNewGallery' => __( 'Create a new gallery' ), 'createNewPlaylist' => __( 'Create a new playlist' ), 'createNewVideoPlaylist' => __( 'Create a new video playlist' ), 'returnToLibrary' => __( '← Go to library' ), 'allMediaItems' => __( 'All media items' ), 'allDates' => __( 'All dates' ), 'noItemsFound' => __( 'No items found.' ), 'insertIntoPost' => $post_type_object->labels->insert_into_item, 'unattached' => _x( 'Unattached', 'media items' ), 'mine' => _x( 'Mine', 'media items' ), 'trash' => _x( 'Trash', 'noun' ), 'uploadedToThisPost' => $post_type_object->labels->uploaded_to_this_item, 'warnDelete' => __( "You are about to permanently delete this item from your site.\nThis action cannot be undone.\n 'Cancel' to stop, 'OK' to delete." ), 'warnBulkDelete' => __( "You are about to permanently delete these items from your site.\nThis action cannot be undone.\n 'Cancel' to stop, 'OK' to delete." ), 'warnBulkTrash' => __( "You are about to trash these items.\n 'Cancel' to stop, 'OK' to delete." ), 'bulkSelect' => __( 'Bulk select' ), 'trashSelected' => __( 'Move to Trash' ), 'restoreSelected' => __( 'Restore from Trash' ), 'deletePermanently' => __( 'Delete permanently' ), 'errorDeleting' => __( 'Error in deleting the attachment.' ), 'apply' => __( 'Apply' ), 'filterByDate' => __( 'Filter by date' ), 'filterByType' => __( 'Filter by type' ), 'searchLabel' => __( 'Search' ), 'searchMediaLabel' => __( 'Search media' ), // Backward compatibility pre-5.3. 'searchMediaPlaceholder' => __( 'Search media items...' ), // Placeholder (no ellipsis), backward compatibility pre-5.3. /* translators: %d: Number of attachments found in a search. */ 'mediaFound' => __( 'Number of media items found: %d' ), 'noMedia' => __( 'No media items found.' ), 'noMediaTryNewSearch' => __( 'No media items found. Try a different search.' ), // Library Details. 'attachmentDetails' => __( 'Attachment details' ), // From URL. 'insertFromUrlTitle' => __( 'Insert from URL' ), // Featured Images. 'setFeaturedImageTitle' => $post_type_object->labels->featured_image, 'setFeaturedImage' => $post_type_object->labels->set_featured_image, // Gallery. 'createGalleryTitle' => __( 'Create gallery' ), 'editGalleryTitle' => __( 'Edit gallery' ), 'cancelGalleryTitle' => __( '← Cancel gallery' ), 'insertGallery' => __( 'Insert gallery' ), 'updateGallery' => __( 'Update gallery' ), 'addToGallery' => __( 'Add to gallery' ), 'addToGalleryTitle' => __( 'Add to gallery' ), 'reverseOrder' => __( 'Reverse order' ), // Edit Image. 'imageDetailsTitle' => __( 'Image details' ), 'imageReplaceTitle' => __( 'Replace image' ), 'imageDetailsCancel' => __( 'Cancel edit' ), 'editImage' => __( 'Edit image' ), // Crop Image. 'chooseImage' => __( 'Choose image' ), 'selectAndCrop' => __( 'Select and crop' ), 'skipCropping' => __( 'Skip cropping' ), 'cropImage' => __( 'Crop image' ), 'cropYourImage' => __( 'Crop your image' ), 'cropping' => __( 'Cropping…' ), /* translators: 1: Suggested width number, 2: Suggested height number. */ 'suggestedDimensions' => __( 'Suggested image dimensions: %1$s by %2$s pixels.' ), 'cropError' => __( 'There has been an error cropping your image.' ), // Edit Audio. 'audioDetailsTitle' => __( 'Audio details' ), 'audioReplaceTitle' => __( 'Replace audio' ), 'audioAddSourceTitle' => __( 'Add audio source' ), 'audioDetailsCancel' => __( 'Cancel edit' ), // Edit Video. 'videoDetailsTitle' => __( 'Video details' ), 'videoReplaceTitle' => __( 'Replace video' ), 'videoAddSourceTitle' => __( 'Add video source' ), 'videoDetailsCancel' => __( 'Cancel edit' ), 'videoSelectPosterImageTitle' => __( 'Select poster image' ), 'videoAddTrackTitle' => __( 'Add subtitles' ), // Playlist. 'playlistDragInfo' => __( 'Drag and drop to reorder tracks.' ), 'createPlaylistTitle' => __( 'Create audio playlist' ), 'editPlaylistTitle' => __( 'Edit audio playlist' ), 'cancelPlaylistTitle' => __( '← Cancel audio playlist' ), 'insertPlaylist' => __( 'Insert audio playlist' ), 'updatePlaylist' => __( 'Update audio playlist' ), 'addToPlaylist' => __( 'Add to audio playlist' ), 'addToPlaylistTitle' => __( 'Add to Audio Playlist' ), // Video Playlist. 'videoPlaylistDragInfo' => __( 'Drag and drop to reorder videos.' ), 'createVideoPlaylistTitle' => __( 'Create video playlist' ), 'editVideoPlaylistTitle' => __( 'Edit video playlist' ), 'cancelVideoPlaylistTitle' => __( '← Cancel video playlist' ), 'insertVideoPlaylist' => __( 'Insert video playlist' ), 'updateVideoPlaylist' => __( 'Update video playlist' ), 'addToVideoPlaylist' => __( 'Add to video playlist' ), 'addToVideoPlaylistTitle' => __( 'Add to video Playlist' ), // Headings. 'filterAttachments' => __( 'Filter media' ), 'attachmentsList' => __( 'Media list' ), ); /** * Filters the media view settings. * * @since 3.5.0 * * @param array $settings List of media view settings. * @param WP_Post $post Post object. */ $settings = apply_filters( 'media_view_settings', $settings, $post ); /** * Filters the media view strings. * * @since 3.5.0 * * @param string[] $strings Array of media view strings keyed by the name they'll be referenced by in JavaScript. * @param WP_Post $post Post object. */ $strings = apply_filters( 'media_view_strings', $strings, $post ); $strings['settings'] = $settings; /* * Ensure we enqueue media-editor first, that way media-views * is registered internally before we try to localize it. See #24724. */ wp_enqueue_script( 'media-editor' ); wp_localize_script( 'media-views', '_wpMediaViewsL10n', $strings ); wp_enqueue_script( 'media-audiovideo' ); wp_enqueue_style( 'media-views' ); if ( is_admin() ) { wp_enqueue_script( 'mce-view' ); wp_enqueue_script( 'image-edit' ); } wp_enqueue_style( 'imgareaselect' ); wp_plupload_default_settings(); require_once ABSPATH . WPINC . '/media-template.php'; add_action( 'admin_footer', 'wp_print_media_templates' ); add_action( 'wp_footer', 'wp_print_media_templates' ); add_action( 'customize_controls_print_footer_scripts', 'wp_print_media_templates' ); /** * Fires at the conclusion of wp_enqueue_media(). * * @since 3.5.0 */ do_action( 'wp_enqueue_media' ); } /** * Retrieves media attached to the passed post. * * @since 3.6.0 * * @param string $type Mime type. * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return WP_Post[] Array of media attached to the given post. */ function get_attached_media( $type, $post = 0 ) { $post = get_post( $post ); if ( ! $post ) { return array(); } $args = array( 'post_parent' => $post->ID, 'post_type' => 'attachment', 'post_mime_type' => $type, 'posts_per_page' => -1, 'orderby' => 'menu_order', 'order' => 'ASC', ); /** * Filters arguments used to retrieve media attached to the given post. * * @since 3.6.0 * * @param array $args Post query arguments. * @param string $type Mime type of the desired media. * @param WP_Post $post Post object. */ $args = apply_filters( 'get_attached_media_args', $args, $type, $post ); $children = get_children( $args ); /** * Filters the list of media attached to the given post. * * @since 3.6.0 * * @param WP_Post[] $children Array of media attached to the given post. * @param string $type Mime type of the media desired. * @param WP_Post $post Post object. */ return (array) apply_filters( 'get_attached_media', $children, $type, $post ); } /** * Checks the HTML content for an audio, video, object, embed, or iframe tags. * * @since 3.6.0 * * @param string $content A string of HTML which might contain media elements. * @param string[] $types An array of media types: 'audio', 'video', 'object', 'embed', or 'iframe'. * @return string[] Array of found HTML media elements. */ function get_media_embedded_in_content( $content, $types = null ) { $html = array(); /** * Filters the embedded media types that are allowed to be returned from the content blob. * * @since 4.2.0 * * @param string[] $allowed_media_types An array of allowed media types. Default media types are * 'audio', 'video', 'object', 'embed', and 'iframe'. */ $allowed_media_types = apply_filters( 'media_embedded_in_content_allowed_types', array( 'audio', 'video', 'object', 'embed', 'iframe' ) ); if ( ! empty( $types ) ) { if ( ! is_array( $types ) ) { $types = array( $types ); } $allowed_media_types = array_intersect( $allowed_media_types, $types ); } $tags = implode( '|', $allowed_media_types ); if ( preg_match_all( '#<(?P' . $tags . ')[^<]*?(?:>[\s\S]*?<\/(?P=tag)>|\s*\/>)#', $content, $matches ) ) { foreach ( $matches[0] as $match ) { $html[] = $match; } } return $html; } /** * Retrieves galleries from the passed post's content. * * @since 3.6.0 * * @param int|WP_Post $post Post ID or object. * @param bool $html Optional. Whether to return HTML or data in the array. Default true. * @return array A list of arrays, each containing gallery data and srcs parsed * from the expanded shortcode. */ function get_post_galleries( $post, $html = true ) { $post = get_post( $post ); if ( ! $post ) { return array(); } if ( ! has_shortcode( $post->post_content, 'gallery' ) && ! has_block( 'gallery', $post->post_content ) ) { return array(); } $galleries = array(); if ( preg_match_all( '/' . get_shortcode_regex() . '/s', $post->post_content, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $shortcode ) { if ( 'gallery' === $shortcode[2] ) { $srcs = array(); $shortcode_attrs = shortcode_parse_atts( $shortcode[3] ); if ( ! is_array( $shortcode_attrs ) ) { $shortcode_attrs = array(); } // Specify the post ID of the gallery we're viewing if the shortcode doesn't reference another post already. if ( ! isset( $shortcode_attrs['id'] ) ) { $shortcode[3] .= ' id="' . (int) $post->ID . '"'; } $gallery = do_shortcode_tag( $shortcode ); if ( $html ) { $galleries[] = $gallery; } else { preg_match_all( '#src=([\'"])(.+?)\1#is', $gallery, $src, PREG_SET_ORDER ); if ( ! empty( $src ) ) { foreach ( $src as $s ) { $srcs[] = $s[2]; } } $galleries[] = array_merge( $shortcode_attrs, array( 'src' => array_values( array_unique( $srcs ) ), ) ); } } } } if ( has_block( 'gallery', $post->post_content ) ) { $post_blocks = parse_blocks( $post->post_content ); while ( $block = array_shift( $post_blocks ) ) { $has_inner_blocks = ! empty( $block['innerBlocks'] ); // Skip blocks with no blockName and no innerHTML. if ( ! $block['blockName'] ) { continue; } // Skip non-Gallery blocks. if ( 'core/gallery' !== $block['blockName'] ) { // Move inner blocks into the root array before skipping. if ( $has_inner_blocks ) { array_push( $post_blocks, ...$block['innerBlocks'] ); } continue; } // New Gallery block format as HTML. if ( $has_inner_blocks && $html ) { $block_html = wp_list_pluck( $block['innerBlocks'], 'innerHTML' ); $galleries[] = '
' . implode( ' ', $block_html ) . '
'; continue; } $srcs = array(); // New Gallery block format as an array. if ( $has_inner_blocks ) { $attrs = wp_list_pluck( $block['innerBlocks'], 'attrs' ); $ids = wp_list_pluck( $attrs, 'id' ); foreach ( $ids as $id ) { $url = wp_get_attachment_url( $id ); if ( is_string( $url ) && ! in_array( $url, $srcs, true ) ) { $srcs[] = $url; } } $galleries[] = array( 'ids' => implode( ',', $ids ), 'src' => $srcs, ); continue; } // Old Gallery block format as HTML. if ( $html ) { $galleries[] = $block['innerHTML']; continue; } // Old Gallery block format as an array. $ids = ! empty( $block['attrs']['ids'] ) ? $block['attrs']['ids'] : array(); // If present, use the image IDs from the JSON blob as canonical. if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { $url = wp_get_attachment_url( $id ); if ( is_string( $url ) && ! in_array( $url, $srcs, true ) ) { $srcs[] = $url; } } $galleries[] = array( 'ids' => implode( ',', $ids ), 'src' => $srcs, ); continue; } // Otherwise, extract srcs from the innerHTML. preg_match_all( '#src=([\'"])(.+?)\1#is', $block['innerHTML'], $found_srcs, PREG_SET_ORDER ); if ( ! empty( $found_srcs[0] ) ) { foreach ( $found_srcs as $src ) { if ( isset( $src[2] ) && ! in_array( $src[2], $srcs, true ) ) { $srcs[] = $src[2]; } } } $galleries[] = array( 'src' => $srcs ); } } /** * Filters the list of all found galleries in the given post. * * @since 3.6.0 * * @param array $galleries Associative array of all found post galleries. * @param WP_Post $post Post object. */ return apply_filters( 'get_post_galleries', $galleries, $post ); } /** * Checks a specified post's content for gallery and, if present, return the first * * @since 3.6.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @param bool $html Optional. Whether to return HTML or data. Default is true. * @return string|array Gallery data and srcs parsed from the expanded shortcode. */ function get_post_gallery( $post = 0, $html = true ) { $galleries = get_post_galleries( $post, $html ); $gallery = reset( $galleries ); /** * Filters the first-found post gallery. * * @since 3.6.0 * * @param array $gallery The first-found post gallery. * @param int|WP_Post $post Post ID or object. * @param array $galleries Associative array of all found post galleries. */ return apply_filters( 'get_post_gallery', $gallery, $post, $galleries ); } /** * Retrieves the image srcs from galleries from a post's content, if present. * * @since 3.6.0 * * @see get_post_galleries() * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global `$post`. * @return array A list of lists, each containing image srcs parsed. * from an expanded shortcode */ function get_post_galleries_images( $post = 0 ) { $galleries = get_post_galleries( $post, false ); return wp_list_pluck( $galleries, 'src' ); } /** * Checks a post's content for galleries and return the image srcs for the first found gallery. * * @since 3.6.0 * * @see get_post_gallery() * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global `$post`. * @return string[] A list of a gallery's image srcs in order. */ function get_post_gallery_images( $post = 0 ) { $gallery = get_post_gallery( $post, false ); return empty( $gallery['src'] ) ? array() : $gallery['src']; } /** * Maybe attempts to generate attachment metadata, if missing. * * @since 3.9.0 * * @param WP_Post $attachment Attachment object. */ function wp_maybe_generate_attachment_metadata( $attachment ) { if ( empty( $attachment ) || empty( $attachment->ID ) ) { return; } $attachment_id = (int) $attachment->ID; $file = get_attached_file( $attachment_id ); $meta = wp_get_attachment_metadata( $attachment_id ); if ( empty( $meta ) && file_exists( $file ) ) { $_meta = get_post_meta( $attachment_id ); $_lock = 'wp_generating_att_' . $attachment_id; if ( ! array_key_exists( '_wp_attachment_metadata', $_meta ) && ! get_transient( $_lock ) ) { set_transient( $_lock, $file ); wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) ); delete_transient( $_lock ); } } } /** * Tries to convert an attachment URL into a post ID. * * @since 4.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $url The URL to resolve. * @return int The found post ID, or 0 on failure. */ function attachment_url_to_postid( $url ) { global $wpdb; $dir = wp_get_upload_dir(); $path = $url; $site_url = parse_url( $dir['url'] ); $image_path = parse_url( $path ); // Force the protocols to match if needed. if ( isset( $image_path['scheme'] ) && ( $image_path['scheme'] !== $site_url['scheme'] ) ) { $path = str_replace( $image_path['scheme'], $site_url['scheme'], $path ); } if ( str_starts_with( $path, $dir['baseurl'] . '/' ) ) { $path = substr( $path, strlen( $dir['baseurl'] . '/' ) ); } $sql = $wpdb->prepare( "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value = %s", $path ); $results = $wpdb->get_results( $sql ); $post_id = null; if ( $results ) { // Use the first available result, but prefer a case-sensitive match, if exists. $post_id = reset( $results )->post_id; if ( count( $results ) > 1 ) { foreach ( $results as $result ) { if ( $path === $result->meta_value ) { $post_id = $result->post_id; break; } } } } /** * Filters an attachment ID found by URL. * * @since 4.2.0 * * @param int|null $post_id The post_id (if any) found by the function. * @param string $url The URL being looked up. */ return (int) apply_filters( 'attachment_url_to_postid', $post_id, $url ); } /** * Returns the URLs for CSS files used in an iframe-sandbox'd TinyMCE media view. * * @since 4.0.0 * * @return string[] The relevant CSS file URLs. */ function wpview_media_sandbox_styles() { $version = 'ver=' . get_bloginfo( 'version' ); $mediaelement = includes_url( "js/mediaelement/mediaelementplayer-legacy.min.css?$version" ); $wpmediaelement = includes_url( "js/mediaelement/wp-mediaelement.css?$version" ); return array( $mediaelement, $wpmediaelement ); } /** * Registers the personal data exporter for media. * * @param array[] $exporters An array of personal data exporters, keyed by their ID. * @return array[] Updated array of personal data exporters. */ function wp_register_media_personal_data_exporter( $exporters ) { $exporters['wordpress-media'] = array( 'exporter_friendly_name' => __( 'WordPress Media' ), 'callback' => 'wp_media_personal_data_exporter', ); return $exporters; } /** * Finds and exports attachments associated with an email address. * * @since 4.9.6 * * @param string $email_address The attachment owner email address. * @param int $page Attachment page number. * @return array { * An array of personal data. * * @type array[] $data An array of personal data arrays. * @type bool $done Whether the exporter is finished. * } */ function wp_media_personal_data_exporter( $email_address, $page = 1 ) { // Limit us to 50 attachments at a time to avoid timing out. $number = 50; $page = (int) $page; $data_to_export = array(); $user = get_user_by( 'email', $email_address ); if ( false === $user ) { return array( 'data' => $data_to_export, 'done' => true, ); } $post_query = new WP_Query( array( 'author' => $user->ID, 'posts_per_page' => $number, 'paged' => $page, 'post_type' => 'attachment', 'post_status' => 'any', 'orderby' => 'ID', 'order' => 'ASC', ) ); foreach ( (array) $post_query->posts as $post ) { $attachment_url = wp_get_attachment_url( $post->ID ); if ( $attachment_url ) { $post_data_to_export = array( array( 'name' => __( 'URL' ), 'value' => $attachment_url, ), ); $data_to_export[] = array( 'group_id' => 'media', 'group_label' => __( 'Media' ), 'group_description' => __( 'User’s media data.' ), 'item_id' => "post-{$post->ID}", 'data' => $post_data_to_export, ); } } $done = $post_query->max_num_pages <= $page; return array( 'data' => $data_to_export, 'done' => $done, ); } /** * Adds additional default image sub-sizes. * * These sizes are meant to enhance the way WordPress displays images on the front-end on larger, * high-density devices. They make it possible to generate more suitable `srcset` and `sizes` attributes * when the users upload large images. * * The sizes can be changed or removed by themes and plugins but that is not recommended. * The size "names" reflect the image dimensions, so changing the sizes would be quite misleading. * * @since 5.3.0 * @access private */ function _wp_add_additional_image_sizes() { // 2x medium_large size. add_image_size( '1536x1536', 1536, 1536 ); // 2x large size. add_image_size( '2048x2048', 2048, 2048 ); } /** * Callback to enable showing of the user error when uploading .heic images. * * @since 5.5.0 * * @param array[] $plupload_settings The settings for Plupload.js. * @return array[] Modified settings for Plupload.js. */ function wp_show_heic_upload_error( $plupload_settings ) { $plupload_settings['heic_upload_error'] = true; return $plupload_settings; } /** * Allows PHP's getimagesize() to be debuggable when necessary. * * @since 5.7.0 * @since 5.8.0 Added support for WebP images. * @since 6.5.0 Added support for AVIF images. * * @param string $filename The file path. * @param array $image_info Optional. Extended image information (passed by reference). * @return array|false Array of image information or false on failure. */ function wp_getimagesize( $filename, array &$image_info = null ) { // Don't silence errors when in debug mode, unless running unit tests. if ( defined( 'WP_DEBUG' ) && WP_DEBUG && ! defined( 'WP_RUN_CORE_TESTS' ) ) { if ( 2 === func_num_args() ) { $info = getimagesize( $filename, $image_info ); } else { $info = getimagesize( $filename ); } } else { /* * Silencing notice and warning is intentional. * * getimagesize() has a tendency to generate errors, such as * "corrupt JPEG data: 7191 extraneous bytes before marker", * even when it's able to provide image size information. * * See https://core.trac.wordpress.org/ticket/42480 */ if ( 2 === func_num_args() ) { $info = @getimagesize( $filename, $image_info ); } else { $info = @getimagesize( $filename ); } } if ( ! empty( $info ) && // Some PHP versions return 0x0 sizes from `getimagesize` for unrecognized image formats, including AVIFs. ! ( empty( $info[0] ) && empty( $info[1] ) ) ) { return $info; } /* * For PHP versions that don't support WebP images, * extract the image size info from the file headers. */ if ( 'image/webp' === wp_get_image_mime( $filename ) ) { $webp_info = wp_get_webp_info( $filename ); $width = $webp_info['width']; $height = $webp_info['height']; // Mimic the native return format. if ( $width && $height ) { return array( $width, $height, IMAGETYPE_WEBP, sprintf( 'width="%d" height="%d"', $width, $height ), 'mime' => 'image/webp', ); } } // For PHP versions that don't support AVIF images, extract the image size info from the file headers. if ( 'image/avif' === wp_get_image_mime( $filename ) ) { $avif_info = wp_get_avif_info( $filename ); $width = $avif_info['width']; $height = $avif_info['height']; // Mimic the native return format. if ( $width && $height ) { return array( $width, $height, IMAGETYPE_AVIF, sprintf( 'width="%d" height="%d"', $width, $height ), 'mime' => 'image/avif', ); } } // The image could not be parsed. return false; } /** * Extracts meta information about an AVIF file: width, height, bit depth, and number of channels. * * @since 6.5.0 * * @param string $filename Path to an AVIF file. * @return array { * An array of AVIF image information. * * @type int|false $width Image width on success, false on failure. * @type int|false $height Image height on success, false on failure. * @type int|false $bit_depth Image bit depth on success, false on failure. * @type int|false $num_channels Image number of channels on success, false on failure. * } */ function wp_get_avif_info( $filename ) { $results = array( 'width' => false, 'height' => false, 'bit_depth' => false, 'num_channels' => false, ); if ( 'image/avif' !== wp_get_image_mime( $filename ) ) { return $results; } // Parse the file using libavifinfo's PHP implementation. require_once ABSPATH . WPINC . '/class-avif-info.php'; $handle = fopen( $filename, 'rb' ); if ( $handle ) { $parser = new Avifinfo\Parser( $handle ); $success = $parser->parse_ftyp() && $parser->parse_file(); fclose( $handle ); if ( $success ) { $results = $parser->features->primary_item_features; } } return $results; } /** * Extracts meta information about a WebP file: width, height, and type. * * @since 5.8.0 * * @param string $filename Path to a WebP file. * @return array { * An array of WebP image information. * * @type int|false $width Image width on success, false on failure. * @type int|false $height Image height on success, false on failure. * @type string|false $type The WebP type: one of 'lossy', 'lossless' or 'animated-alpha'. * False on failure. * } */ function wp_get_webp_info( $filename ) { $width = false; $height = false; $type = false; if ( 'image/webp' !== wp_get_image_mime( $filename ) ) { return compact( 'width', 'height', 'type' ); } $magic = file_get_contents( $filename, false, null, 0, 40 ); if ( false === $magic ) { return compact( 'width', 'height', 'type' ); } // Make sure we got enough bytes. if ( strlen( $magic ) < 40 ) { return compact( 'width', 'height', 'type' ); } /* * The headers are a little different for each of the three formats. * Header values based on WebP docs, see https://developers.google.com/speed/webp/docs/riff_container. */ switch ( substr( $magic, 12, 4 ) ) { // Lossy WebP. case 'VP8 ': $parts = unpack( 'v2', substr( $magic, 26, 4 ) ); $width = (int) ( $parts[1] & 0x3FFF ); $height = (int) ( $parts[2] & 0x3FFF ); $type = 'lossy'; break; // Lossless WebP. case 'VP8L': $parts = unpack( 'C4', substr( $magic, 21, 4 ) ); $width = (int) ( $parts[1] | ( ( $parts[2] & 0x3F ) << 8 ) ) + 1; $height = (int) ( ( ( $parts[2] & 0xC0 ) >> 6 ) | ( $parts[3] << 2 ) | ( ( $parts[4] & 0x03 ) << 10 ) ) + 1; $type = 'lossless'; break; // Animated/alpha WebP. case 'VP8X': // Pad 24-bit int. $width = unpack( 'V', substr( $magic, 24, 3 ) . "\x00" ); $width = (int) ( $width[1] & 0xFFFFFF ) + 1; // Pad 24-bit int. $height = unpack( 'V', substr( $magic, 27, 3 ) . "\x00" ); $height = (int) ( $height[1] & 0xFFFFFF ) + 1; $type = 'animated-alpha'; break; } return compact( 'width', 'height', 'type' ); } /** * Gets loading optimization attributes. * * This function returns an array of attributes that should be merged into the given attributes array to optimize * loading performance. Potential attributes returned by this function are: * - `loading` attribute with a value of "lazy" * - `fetchpriority` attribute with a value of "high" * - `decoding` attribute with a value of "async" * * If any of these attributes are already present in the given attributes, they will not be modified. Note that no * element should have both `loading="lazy"` and `fetchpriority="high"`, so the function will trigger a warning in case * both attributes are present with those values. * * @since 6.3.0 * * @global WP_Query $wp_query WordPress Query object. * * @param string $tag_name The tag name. * @param array $attr Array of the attributes for the tag. * @param string $context Context for the element for which the loading optimization attribute is requested. * @return array Loading optimization attributes. */ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { global $wp_query; /** * Filters whether to short-circuit loading optimization attributes. * * Returning an array from the filter will effectively short-circuit the loading of optimization attributes, * returning that value instead. * * @since 6.4.0 * * @param array|false $loading_attrs False by default, or array of loading optimization attributes to short-circuit. * @param string $tag_name The tag name. * @param array $attr Array of the attributes for the tag. * @param string $context Context for the element for which the loading optimization attribute is requested. */ $loading_attrs = apply_filters( 'pre_wp_get_loading_optimization_attributes', false, $tag_name, $attr, $context ); if ( is_array( $loading_attrs ) ) { return $loading_attrs; } $loading_attrs = array(); /* * Skip lazy-loading for the overall block template, as it is handled more granularly. * The skip is also applicable for `fetchpriority`. */ if ( 'template' === $context ) { /** This filter is documented in wp-includes/media.php */ return apply_filters( 'wp_get_loading_optimization_attributes', $loading_attrs, $tag_name, $attr, $context ); } // For now this function only supports images and iframes. if ( 'img' !== $tag_name && 'iframe' !== $tag_name ) { /** This filter is documented in wp-includes/media.php */ return apply_filters( 'wp_get_loading_optimization_attributes', $loading_attrs, $tag_name, $attr, $context ); } /* * Skip programmatically created images within content blobs as they need to be handled together with the other * images within the post content or widget content. * Without this clause, they would already be considered within their own context which skews the image count and * can result in the first post content image being lazy-loaded or an image further down the page being marked as a * high priority. */ if ( 'the_content' !== $context && doing_filter( 'the_content' ) || 'widget_text_content' !== $context && doing_filter( 'widget_text_content' ) || 'widget_block_content' !== $context && doing_filter( 'widget_block_content' ) ) { /** This filter is documented in wp-includes/media.php */ return apply_filters( 'wp_get_loading_optimization_attributes', $loading_attrs, $tag_name, $attr, $context ); } /* * Add `decoding` with a value of "async" for every image unless it has a * conflicting `decoding` attribute already present. */ if ( 'img' === $tag_name ) { if ( isset( $attr['decoding'] ) ) { $loading_attrs['decoding'] = $attr['decoding']; } else { $loading_attrs['decoding'] = 'async'; } } // For any resources, width and height must be provided, to avoid layout shifts. if ( ! isset( $attr['width'], $attr['height'] ) ) { /** This filter is documented in wp-includes/media.php */ return apply_filters( 'wp_get_loading_optimization_attributes', $loading_attrs, $tag_name, $attr, $context ); } /* * The key function logic starts here. */ $maybe_in_viewport = null; $increase_count = false; $maybe_increase_count = false; // Logic to handle a `loading` attribute that is already provided. if ( isset( $attr['loading'] ) ) { /* * Interpret "lazy" as not in viewport. Any other value can be * interpreted as in viewport (realistically only "eager" or `false` * to force-omit the attribute are other potential values). */ if ( 'lazy' === $attr['loading'] ) { $maybe_in_viewport = false; } else { $maybe_in_viewport = true; } } // Logic to handle a `fetchpriority` attribute that is already provided. if ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] ) { /* * If the image was already determined to not be in the viewport (e.g. * from an already provided `loading` attribute), trigger a warning. * Otherwise, the value can be interpreted as in viewport, since only * the most important in-viewport image should have `fetchpriority` set * to "high". */ if ( false === $maybe_in_viewport ) { _doing_it_wrong( __FUNCTION__, __( 'An image should not be lazy-loaded and marked as high priority at the same time.' ), '6.3.0' ); /* * Set `fetchpriority` here for backward-compatibility as we should * not override what a developer decided, even though it seems * incorrect. */ $loading_attrs['fetchpriority'] = 'high'; } else { $maybe_in_viewport = true; } } if ( null === $maybe_in_viewport ) { $header_enforced_contexts = array( 'template_part_' . WP_TEMPLATE_PART_AREA_HEADER => true, 'get_header_image_tag' => true, ); /** * Filters the header-specific contexts. * * @since 6.4.0 * * @param array $default_header_enforced_contexts Map of contexts for which elements should be considered * in the header of the page, as $context => $enabled * pairs. The $enabled should always be true. */ $header_enforced_contexts = apply_filters( 'wp_loading_optimization_force_header_contexts', $header_enforced_contexts ); // Consider elements with these header-specific contexts to be in viewport. if ( isset( $header_enforced_contexts[ $context ] ) ) { $maybe_in_viewport = true; $maybe_increase_count = true; } elseif ( ! is_admin() && in_the_loop() && is_main_query() ) { /* * Get the content media count, since this is a main query * content element. This is accomplished by "increasing" * the count by zero, as the only way to get the count is * to call this function. * The actual count increase happens further below, based * on the `$increase_count` flag set here. */ $content_media_count = wp_increase_content_media_count( 0 ); $increase_count = true; // If the count so far is below the threshold, `loading` attribute is omitted. if ( $content_media_count < wp_omit_loading_attr_threshold() ) { $maybe_in_viewport = true; } else { $maybe_in_viewport = false; } } elseif ( // Only apply for main query but before the loop. $wp_query->before_loop && $wp_query->is_main_query() /* * Any image before the loop, but after the header has started should not be lazy-loaded, * except when the footer has already started which can happen when the current template * does not include any loop. */ && did_action( 'get_header' ) && ! did_action( 'get_footer' ) ) { $maybe_in_viewport = true; $maybe_increase_count = true; } } /* * If the element is in the viewport (`true`), potentially add * `fetchpriority` with a value of "high". Otherwise, i.e. if the element * is not not in the viewport (`false`) or it is unknown (`null`), add * `loading` with a value of "lazy". */ if ( $maybe_in_viewport ) { $loading_attrs = wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ); } else { // Only add `loading="lazy"` if the feature is enabled. if ( wp_lazy_loading_enabled( $tag_name, $context ) ) { $loading_attrs['loading'] = 'lazy'; } } /* * If flag was set based on contextual logic above, increase the content * media count, either unconditionally, or based on whether the image size * is larger than the threshold. */ if ( $increase_count ) { wp_increase_content_media_count(); } elseif ( $maybe_increase_count ) { /** This filter is documented in wp-includes/media.php */ $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { wp_increase_content_media_count(); } } /** * Filters the loading optimization attributes. * * @since 6.4.0 * * @param array $loading_attrs The loading optimization attributes. * @param string $tag_name The tag name. * @param array $attr Array of the attributes for the tag. * @param string $context Context for the element for which the loading optimization attribute is requested. */ return apply_filters( 'wp_get_loading_optimization_attributes', $loading_attrs, $tag_name, $attr, $context ); } /** * Gets the threshold for how many of the first content media elements to not lazy-load. * * This function runs the {@see 'wp_omit_loading_attr_threshold'} filter, which uses a default threshold value of 3. * The filter is only run once per page load, unless the `$force` parameter is used. * * @since 5.9.0 * * @param bool $force Optional. If set to true, the filter will be (re-)applied even if it already has been before. * Default false. * @return int The number of content media elements to not lazy-load. */ function wp_omit_loading_attr_threshold( $force = false ) { static $omit_threshold; // This function may be called multiple times. Run the filter only once per page load. if ( ! isset( $omit_threshold ) || $force ) { /** * Filters the threshold for how many of the first content media elements to not lazy-load. * * For these first content media elements, the `loading` attribute will be omitted. By default, this is the case * for only the very first content media element. * * @since 5.9.0 * @since 6.3.0 The default threshold was changed from 1 to 3. * * @param int $omit_threshold The number of media elements where the `loading` attribute will not be added. Default 3. */ $omit_threshold = apply_filters( 'wp_omit_loading_attr_threshold', 3 ); } return $omit_threshold; } /** * Increases an internal content media count variable. * * @since 5.9.0 * @access private * * @param int $amount Optional. Amount to increase by. Default 1. * @return int The latest content media count, after the increase. */ function wp_increase_content_media_count( $amount = 1 ) { static $content_media_count = 0; $content_media_count += $amount; return $content_media_count; } /** * Determines whether to add `fetchpriority='high'` to loading attributes. * * @since 6.3.0 * @access private * * @param array $loading_attrs Array of the loading optimization attributes for the element. * @param string $tag_name The tag name. * @param array $attr Array of the attributes for the element. * @return array Updated loading optimization attributes for the element. */ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ) { // For now, adding `fetchpriority="high"` is only supported for images. if ( 'img' !== $tag_name ) { return $loading_attrs; } if ( isset( $attr['fetchpriority'] ) ) { /* * While any `fetchpriority` value could be set in `$loading_attrs`, * for consistency we only do it for `fetchpriority="high"` since that * is the only possible value that WordPress core would apply on its * own. */ if ( 'high' === $attr['fetchpriority'] ) { $loading_attrs['fetchpriority'] = 'high'; wp_high_priority_element_flag( false ); } return $loading_attrs; } // Lazy-loading and `fetchpriority="high"` are mutually exclusive. if ( isset( $loading_attrs['loading'] ) && 'lazy' === $loading_attrs['loading'] ) { return $loading_attrs; } if ( ! wp_high_priority_element_flag() ) { return $loading_attrs; } /** * Filters the minimum square-pixels threshold for an image to be eligible as the high-priority image. * * @since 6.3.0 * * @param int $threshold Minimum square-pixels threshold. Default 50000. */ $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { $loading_attrs['fetchpriority'] = 'high'; wp_high_priority_element_flag( false ); } return $loading_attrs; } /** * Accesses a flag that indicates if an element is a possible candidate for `fetchpriority='high'`. * * @since 6.3.0 * @access private * * @param bool $value Optional. Used to change the static variable. Default null. * @return bool Returns true if high-priority element was marked already, otherwise false. */ function wp_high_priority_element_flag( $value = null ) { static $high_priority_element = true; if ( is_bool( $value ) ) { $high_priority_element = $value; } return $high_priority_element; } et_controller('totp')->potentially_port_private_keys(); } /** * Give the filesystem path to the plugin's includes directory * * @return String */ public function includes_dir() { return __DIR__.'/includes'; } /** * Give the URL for the plugin's includes directory * * @return String */ public function includes_url() { return plugins_url('', __FILE__).'/includes'; } /** * Set URL slug for the plugin's option page. * * @param String Setting page URL slug. * @return Void */ public function set_user_settings_page_slug($user_settings_page_slug) { $this->user_settings_page_slug = $user_settings_page_slug; } /** * Get URL slug for the plugin's option page. * * @return String Setting page URL slug. */ public function get_user_settings_page_slug() { return $this->user_settings_page_slug; } /** * Set settings page heading for plugin's option page * * @param String $settings_page_heading String. * * @return String */ public function set_settings_page_heading($settings_page_heading) { $this->settings_page_heading = $settings_page_heading; } /** * Get settings page heading for plugin's option page. * * @return String Setting page heading. */ public function get_settings_page_heading() { return $this->settings_page_heading; } /** * Set plugin translate url * * @param String $plugin_translate_url Plugin translation URL. * @return Void */ public function set_plugin_translate_url($plugin_translate_url) { $this->plugin_translate_url = $plugin_translate_url; } /** * Get plugin translate url * * @return String Plugin translate URL */ public function get_plugin_translate_url() { return $this->plugin_translate_url; } /** * Set plugin premium version url * * @param String $premium_version_url Plugin premium version url. * @return Void */ public function set_premium_version_url($premium_version_url) { $this->premium_version_url = $premium_version_url; } /** * Get plugin premium version URL. * * @return String Plugin premium version URL. */ public function get_premium_version_url() { return $this->premium_version_url; } /** * Set plugin FAQ URL * * @param String $faq_url Plugin FAQ URL. * @return Void */ public function set_faq_url($faq_url) { $this->faq_url = $faq_url; } /** * Get plugin FAQ URL. * * @return String Plugin FAQ URL. */ public function get_faq_url() { return $this->faq_url; } /** * Set plugin site wide administration URL * * @param String $site_wide_administration_url Plugin site wide administration URL. * @return Void */ public function set_site_wide_administration_url($site_wide_administration_url) { $this->site_wide_administration_url = $site_wide_administration_url; } /** * Get plugin site wide administration URL. * * @return String Plugin site wide administration URL */ public function get_site_wide_administration_url() { return $this->site_wide_administration_url; } /** * Give the filesystem path to the plugin's templates directory * * @return String */ public function templates_dir() { return __DIR__.'/templates'; } /** * Include the user settings page code */ public function show_dashboard_user_settings_page() { $this->include_template('user-settings.php'); } /** * Enqueue CSS styling on the users page */ public function load_users_css() { $css_version = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/users.css'); wp_enqueue_style( 'tfa-users-css', $this->includes_url().'/users.css', array(), $css_version, 'screen' ); } /** * Add the 2FA label to the users list table header. * * @param Array $columns Table columns. * * @return Array */ public function manage_users_columns_tfa($columns = array()) { $columns['tfa-status'] = __('2FA', 'all-in-one-wp-security-and-firewall'); return $columns; } /** * Add status into TFA column. * * @param String $value String. * @param String $column_name Column name. * @param Integer $user_id User ID. * * @return String */ public function manage_users_custom_column_tfa($value = '', $column_name = '', $user_id = 0) { // Only for this column name. if ('tfa-status' === $column_name) { if (!$this->is_activated_for_user($user_id)) { $value = '—'; } elseif ($this->is_activated_by_user($user_id)) { // Use value. $value = ''; } else { // No group. $value = ''; } } return $value; } /** * Paint out an admin notice * * @param String $message - the caller should already have taken care of any escaping * @param String $class */ public function show_admin_warning($message, $class = 'updated') { echo '
'."

$message

"; } /** * Returns all two factor authentication setting name => group pairs. * * @return Array */ private function get_config_keys() { global $wp_roles; if (!isset($wp_roles)) $wp_roles = new WP_Roles(); $keys = array( 'tfa_requireafter' => 'tfa_user_roles_required_group', 'tfa_require_enforce_after' => 'tfa_user_roles_required_group', 'tfa_if_required_redirect_to' => 'tfa_user_roles_required_group', 'tfa_hide_turn_off' => 'tfa_user_roles_required_group', 'tfa_trusted_for' => 'tfa_user_roles_trusted_group', 'tfa_wc_add_section' => 'simba_tfa_woocommerce_group', 'tfa_bot_protection' => 'simba_tfa_woocommerce_group', 'tfa_default_hmac' => 'simba_tfa_default_hmac_group', 'tfa_xmlrpc_on' => 'tfa_xmlrpc_status_group', ); foreach ($wp_roles->role_names as $id => $name) { $keys['tfa_'.$id] = 'tfa_user_roles_group'; $keys['tfa_trusted_'.$id] = 'tfa_user_roles_trusted_group'; $keys['tfa_required_'.$id] = 'tfa_user_roles_required_group'; } if (is_multisite()) { $keys['tfa__super_admin'] = 'tfa_user_roles_group'; $keys['tfa_trusted__super_admin'] = 'tfa_user_roles_trusted_group'; $keys['tfa_required__super_admin'] = 'tfa_user_roles_required_group'; } return $keys; } /** * Registers all two factor authentication settings. Runs upon the WP action admin_init. */ public function register_two_factor_auth_settings() { $config_keys = $this->get_config_keys(); foreach ($config_keys as $name => $group) { register_setting($group, $name); } } /** * Returns all two factor authentication options from the WP database. * * @return Array */ public function get_configs() { $config_keys = $this->get_config_keys(); $configs = array(); foreach (array_keys($config_keys) as $name) { if (false !== $this->get_option($name)) { $configs[$name] = $this->get_option($name); } } return $configs; } /** * Sets two factor authentication options from array. * * @param Array $configs * * @return Boolean */ public function set_configs($configs) { $result = false; foreach ($configs as $key => $value) { $result = $this->update_option($key, $value) ? true : $result; } return $result; } /** * Deletes all two factor authentication options from the WP database. * * @return Void */ public function delete_configs() { $config_keys = $this->get_config_keys(); foreach (array_keys($config_keys) as $name) { $this->delete_option($name); } } /** * See whether TFA is available or not for a particular user - i.e. whether the administrator has permitted it for their user level * * @param Integer $user_id - WordPress user ID * * @return Boolean */ public function is_activated_for_user($user_id) { if (empty($user_id)) return false; // Super admin is not a role (they are admins with an extra attribute); needs separate handling if (is_multisite() && is_super_admin($user_id)) { // This is always a final decision - we don't want it to drop through to the 'admin' role's setting $role = '_super_admin'; $db_val = $this->get_option('tfa_'.$role); // Defaults to true if no setting has been saved return (false === $db_val || $db_val) ? true : false; } $roles = $this->get_user_roles($user_id); // N.B. This populates with roles on the current site within a multisite foreach ($roles as $role) { $db_val = $this->get_option('tfa_'.$role); if (false === $db_val || $db_val) return true; } return false; } /** * Get all user roles for a given user (if on multisite, amalgamates all roles from all sites) * * @param Integer $user_id - WordPress user ID * * @return Array */ protected function get_user_roles($user_id) { // Get roles on the main site $user = new WP_User($user_id); $roles = (array) $user->roles; // On multisite, also check roles on non-main sites if (is_multisite()) { global $wpdb, $table_prefix; $roles_db = $wpdb->get_results($wpdb->prepare("SELECT meta_key, meta_value FROM {$wpdb->usermeta} WHERE user_id=%d AND meta_key LIKE '".esc_sql($table_prefix)."%_capabilities'", $user_id)); if (is_array($roles_db)) { foreach ($roles_db as $role_info) { if (empty($role_info->meta_key) || !preg_match('/^'.$table_prefix.'\d+_capabilities$/', $role_info->meta_key) || empty($role_info->meta_value) || !preg_match('/^a:/', $role_info->meta_value)) continue; $site_roles = unserialize($role_info->meta_value); if (!is_array($site_roles)) continue; foreach ($site_roles as $role => $active) { if ($active && !in_array($role, $roles)) $roles[] = $role; } } } } return $roles; } /** * Check if TFA is required for a specified user * * N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first * * @param $user_id Integer - the WP user ID * * @return Boolean */ public function is_required_for_user($user_id) { return apply_filters('simba_tfa_required_for_user', $this->user_property_active($user_id, 'required_'), $user_id); } /** * See if a particular user property is active * * @param Integer $user_id * @param String $prefix - e.g. "required_", "trusted_" * * @return Boolean */ public function user_property_active($user_id, $prefix = 'required_') { if (empty($user_id)) return false; // Super admin is not a role (they are admins with an extra attribute); needs separate handling if (is_multisite() && is_super_admin($user_id)) { // This is always a final decision - we don't want it to drop through to the 'admin' role's setting $role = '_super_admin'; $db_val = $this->get_option('tfa_'.$prefix.$role); return $db_val ? true : false; } $roles = $this->get_user_roles($user_id); foreach ($roles as $role) { $db_val = $this->get_option('tfa_'.$prefix.$role); if ($db_val) return true; } return false; } /** * Whether TFA is activated by a specific user. Note that this doesn't check if TFA is enabled for the user's role; the caller should check that first. * * @param Integer $user_id * * @return Boolean */ public function is_activated_by_user($user_id) { $enabled = get_user_meta($user_id, 'tfa_enable_tfa', true); return !empty($enabled); } /** * Get a list of trusted devices for the user * * @param Integer|Boolean $user_id - WordPress user ID, or false for the current user * * @return Array */ public function user_get_trusted_devices($user_id = false) { if (false === $user_id) { global $current_user; $user_id = $current_user->ID; } $trusted_devices = get_user_meta($user_id, 'tfa_trusted_devices', true); if (!is_array($trusted_devices)) $trusted_devices = array(); return $trusted_devices; } /** * Trust the current device * * @param Integer $user_id - WordPress user ID * @param Integer $trusted_for - time to trust for, in days */ public function trust_device($user_id, $trusted_for) { $trusted_devices = $this->user_get_trusted_devices($user_id); $time_now = time(); foreach ($trusted_devices as $k => $device) { if (empty($device['until']) || $device['until'] <= $time_now) unset($trusted_devices[$k]); } $until = $time_now + $trusted_for * 86400; $token = bin2hex($this->random_bytes(40)); $trusted_devices[] = array( 'ip' => $_SERVER['REMOTE_ADDR'], 'until' => $until, 'user_agent' => empty($_SERVER['HTTP_USER_AGENT']) ? '' : (string) $_SERVER['HTTP_USER_AGENT'], 'token' => $token ); $this->user_set_trusted_devices($user_id, $trusted_devices); $this->set_cookie('simbatfa_trust_token', $token, $until); } /** * Returns true if running on a PHP version on which mcrypt has been deprecated * * @return Boolean */ public function is_mcrypt_deprecated() { return (7 == PHP_MAJOR_VERSION && PHP_MINOR_VERSION >= 1); } /** * Return the specified number of bytes * * @param Integer $bytes * * @throws Exception * * @return String */ public function random_bytes($bytes) { if (function_exists('random_bytes')) { return random_bytes($bytes); } elseif (function_exists('mcrypt_create_iv')) { return $this->is_mcrypt_deprecated() ? @mcrypt_create_iv($bytes, MCRYPT_RAND) : mcrypt_create_iv($bytes, MCRYPT_RAND); } elseif (function_exists('openssl_random_pseudo_bytes')) { return openssl_random_pseudo_bytes($bytes); } throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed'); } /** * Set a cookie so that, however we logged in, it can be found * * @param String $name - the cookie name * @param String $value - the cookie value * @param Integer $expires - when the cookie expires, in epoch time. Defaults to 24 hours' time. Values in the past cause cookie deletion. */ protected function set_cookie($name, $value, $expires = null) { if (null === $expires) $expires = time() + 86400; $secure = is_ssl(); $secure_logged_in_cookie = ($secure && 'https' === parse_url(get_option('home'), PHP_URL_SCHEME)); $secure = apply_filters('secure_auth_cookie', $secure, get_current_user_id()); $secure_logged_in_cookie = apply_filters('secure_logged_in_cookie', $secure_logged_in_cookie, get_current_user_id(), $secure); setcookie($name, $value, $expires, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, $secure, true); setcookie($name, $value, $expires, COOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true); if (COOKIEPATH != SITECOOKIEPATH) { setcookie($name, $value, $expires, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true); } } /** * Get a list of trusted devices for the user * * @param Integer $user_id - WordPress user ID * @param Array $trusted_devices - the list of devices */ public function user_set_trusted_devices($user_id, $trusted_devices) { update_user_meta($user_id, 'tfa_trusted_devices', $trusted_devices); } /** * Get the user capability needed for managing TFA users. * You'll want to think carefully about changing this to a non-admin, as it can give the ability to lock admins out (though, if you have FTP/files access, you can always disable TFA or any plugin) * * @return String */ public function get_management_capability() { return apply_filters('simba_tfa_management_capability', 'manage_options'); } /** * Used with set_error_handler() * * @param Integer $errno * @param String $errstr * @param String $errfile * @param Integer $errline * * @return Boolean */ public function get_php_errors($errno, $errstr, $errfile, $errline) { if (0 == error_reporting()) return true; $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline); $this->logged[] = $logline; # Don't pass it up the chain (since it's going to be output to the user always) return true; } public function php_error_to_logline($errno, $errstr, $errfile, $errline) { switch ($errno) { case 1: $e_type = 'E_ERROR'; break; case 2: $e_type = 'E_WARNING'; break; case 4: $e_type = 'E_PARSE'; break; case 8: $e_type = 'E_NOTICE'; break; case 16: $e_type = 'E_CORE_ERROR'; break; case 32: $e_type = 'E_CORE_WARNING'; break; case 64: $e_type = 'E_COMPILE_ERROR'; break; case 128: $e_type = 'E_COMPILE_WARNING'; break; case 256: $e_type = 'E_USER_ERROR'; break; case 512: $e_type = 'E_USER_WARNING'; break; case 1024: $e_type = 'E_USER_NOTICE'; break; case 2048: $e_type = 'E_STRICT'; break; case 4096: $e_type = 'E_RECOVERABLE_ERROR'; break; case 8192: $e_type = 'E_DEPRECATED'; break; case 16384: $e_type = 'E_USER_DEPRECATED'; break; case 30719: $e_type = 'E_ALL'; break; default: $e_type = "E_UNKNOWN ($errno)"; break; } if (!is_string($errstr)) $errstr = serialize($errstr); if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH)); return "PHP event: code $e_type: $errstr (line $errline, $errfile)"; } /** * Runs upon the WordPress 'init' action */ public function init() { if ((!is_admin() || (defined('DOING_AJAX') && DOING_AJAX)) && is_user_logged_in() && file_exists($this->includes_dir().'/tfa_frontend.php')) { $this->load_frontend(); } else { add_shortcode('twofactor_user_settings', array($this, 'shortcode_when_not_logged_in')); } } /** * Return the TOTP provider object. * * @param String $controller_id - which controller * * @return Simba_TFA_Provider_totp */ public function get_controller($controller_id = 'totp') { return $this->controllers[$controller_id]; } /** * Return all OTP controllers * * @return Array */ public function get_controllers() { return $this->controllers; } /** * Deprecated synonym for get_controller('totp') * * @return Simba_TFA_Provider_totp */ public function get_totp_controller() { trigger_error("Deprecated: Call get_controller('totp'), not get_totp_controller()", E_USER_WARNING); return $this->get_controller('totp'); } /** * "Shared" - i.e. could be called from either front-end or back-end */ public function shared_ajax() { if (empty($_POST['subaction']) || empty($_POST['nonce']) || !is_user_logged_in() || !wp_verify_nonce($_POST['nonce'], 'tfa_shared_nonce')) die('Security check (3).'); global $current_user; $subaction = $_POST['subaction']; if ('refreshotp' == $subaction) { $code = $this->get_controller('totp')->get_current_code($current_user->ID); if (false === $code) die(json_encode(array('code' => ''))); die(json_encode(array('code' => $code))); } elseif ('untrust_device' == $subaction && isset($_POST['device_id'])) { $this->untrust_device(stripslashes($_POST['device_id'])); ob_start(); $this->include_template('trusted-devices-inner-box.php', array('trusted_devices' => $this->user_get_trusted_devices())); echo json_encode(array('trusted_list' => ob_get_clean())); } exit; } /** * Mark a device as untrusted for the current user * * @param String $device_id */ protected function untrust_device($device_id) { $trusted_devices = $this->user_get_trusted_devices(); unset($trusted_devices[$device_id]); global $current_user; $current_user_id = $current_user->ID; $this->user_set_trusted_devices($current_user_id, $trusted_devices); } /** * Called upon the AJAX action simbatfa-init-otp . Will die. * * Uses these keys from $_POST: user */ public function tfaInitLogin() { if (empty($_POST['user'])) die('Security check (2).'); if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) { $res = array('result' => false, 'user_can_trust' => false); } else { if (!function_exists('sanitize_user')) require_once ABSPATH.WPINC.'/formatting.php'; // WP's password-checking sanitizes the supplied user, so we must do the same to check if TFA is enabled for them $auth_info = array('log' => sanitize_user(stripslashes((string)$_POST['user']))); if (!empty($_COOKIE['simbatfa_trust_token'])) $auth_info['trust_token'] = (string) $_COOKIE['simbatfa_trust_token']; $res = $this->pre_auth($auth_info, 'array'); } $results = array( 'jsonstarter' => 'justhere', 'status' => $res['result'], ); if (!empty($res['user_can_trust'])) { $results['user_can_trust'] = 1; if (!empty($res['user_already_trusted'])) $results['user_already_trusted'] = 1; } if (!empty($this->output_buffering)) { if (!empty($this->logged)) { $results['php_output'] = $this->logged; } restore_error_handler(); $buffered = ob_get_clean(); if ($buffered) $results['extra_output'] = $buffered; } $results = apply_filters('simbatfa_check_tfa_requirements_ajax_response', $results); echo json_encode($results); exit; } /** * Enable or disable TFA for a user * * @param Integer $user_id - the WordPress user ID * @param String $setting - either "true" (to turn on) or "false" (to turn off) */ public function change_tfa_enabled_status($user_id, $setting) { $previously_enabled = $this->is_activated_by_user($user_id) ? 1 : 0; $setting = ('true' === $setting) ? 1 : 0; update_user_meta($user_id, 'tfa_enable_tfa', $setting); do_action('simba_tfa_activation_status_saved', $user_id, $setting, $previously_enabled, $this); } /** * Here's where the login action happens. Called on the WP 'authenticate' action (which also happens when wp-login.php loads, so parameters need checking). * * @param WP_Error|WP_User $user * @param String $username - this is not necessarily the WP username; it is whatever was typed in the form, so can be an email address * @param String $password * * @return WP_Error|WP_User */ public function tfaVerifyCodeAndUser($user, $username, $password) { // When both the AIOWPS and Two Factor Authentication plugins are active, this function is called more than once; that should be short-circuited. if (isset(self::$is_authenticated[$this->authentication_slug]) && self::$is_authenticated[$this->authentication_slug]) { return $user; } $original_user = $user; $params = stripslashes_deep($_POST); // If (only) the error was a wrong password, but it looks like the user appended a TFA code to their password, then have another go if (is_wp_error($user) && array('incorrect_password') == $user->get_error_codes() && !isset($params['two_factor_code']) && false !== ($from_password = apply_filters('simba_tfa_tfa_from_password', false, $password))) { // This forces a new password authentication below $user = false; } if (is_wp_error($user)) { $ret = $user; } else { if (is_object($user) && isset($user->ID) && isset($user->user_login)) { $params['log'] = $user->user_login; // Confirm that this is definitely a username regardless of its format $may_be_email = false; } else { $params['log'] = $username; $may_be_email = true; } $params['caller'] = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['REQUEST_URI']; if (!empty($_COOKIE['simbatfa_trust_token'])) $params['trust_token'] = (string) $_COOKIE['simbatfa_trust_token']; if (isset($from_password) && false !== $from_password) { // Support login forms that can't be hooked via appending to the password $speculatively_try_appendage = true; $params['two_factor_code'] = $from_password['tfa_code']; } $code_ok = $this->authorise_user_from_login($params, $may_be_email); if (is_wp_error($code_ok)) { $ret = $code_ok; } elseif (!$code_ok) { $ret = new WP_Error('authentication_failed', ''.__('Error:', 'all-in-one-wp-security-and-firewall').' '.apply_filters('simba_tfa_message_code_incorrect', __('The one-time password (TFA code) you entered was incorrect.', 'all-in-one-wp-security-and-firewall'))); } elseif ($user) { $ret = $user; } else { if (!empty($speculatively_try_appendage) && true === $code_ok) { $password = $from_password['password']; } $username_is_email = false; if (function_exists('wp_authenticate_username_password') && $may_be_email && filter_var($username, FILTER_VALIDATE_EMAIL)) { global $wpdb; // This has to match self::authorise_user_from_login() $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $username)); if (is_object($response)) $username_is_email = true; } $ret = $username_is_email ? wp_authenticate_email_password(null, $username, $password) : wp_authenticate_username_password(null, $username, $password); } } $ret = apply_filters('simbatfa_verify_code_and_user_result', $ret, $original_user, $username, $password); // If the TFA code was actually validated (not just not required, for example), then $code_ok is (boolean)true if (isset($code_ok) && true === $code_ok && is_a($ret, 'WP_User')) { // Though $_SERVER['SERVER_NAME'] can't always be trusted (if the webserver is misconfigured), anyone using this already has password and TFA clearance. if (!empty($params['simba_tfa_mark_as_trusted']) && $this->user_can_trust($ret->ID) && (is_ssl() || (!empty($_SERVER['SERVER_NAME']) && ('localhost' == $_SERVER['SERVER_NAME'] ||'127.0.0.1' == $_SERVER['SERVER_NAME'] || preg_match('/\.localdomain$/', $_SERVER['SERVER_NAME']))))) { $trusted_for = $this->get_option('tfa_trusted_for'); $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for); $this->trust_device($ret->ID, $trusted_for); } } self::$is_authenticated[$this->authentication_slug] = true; return $ret; } // N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first public function user_can_trust($user_id) { // Default is false because this is a new feature and we don't want to surprise existing users by granting broader access than they expected upon an upgrade return apply_filters('simba_tfa_user_can_trust', false, $user_id); } /** * Should the user be asked for a TFA code? And optionally, is the user allowed to trust devices? * * @param Array $params - the key used is 'log', indicating the username or email address * @param String $response_format - 'simple' (historic format) or 'array' (richer info) * * @return Boolean */ public function pre_auth($params, $response_format = 'simple') { global $wpdb; $query = filter_var($params['log'], FILTER_VALIDATE_EMAIL) ? $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_email=%s", $params['log']) : $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']); $user = $wpdb->get_row($query); if (!$user && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) { // Corner-case: login looks like an email, but is a username rather than email address $user = $wpdb->get_row($wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log'])); } $is_activated_for_user = true; $is_activated_by_user = false; $result = false; $totp_controller = $this->get_controller('totp'); if ($user) { $tfa_priv_key = get_user_meta($user->ID, 'tfa_priv_key_64', true); $is_activated_for_user = $this->is_activated_for_user($user->ID); $is_activated_by_user = $this->is_activated_by_user($user->ID); if ($is_activated_for_user && $is_activated_by_user) { // No private key yet, generate one. This shouldn't really be possible. if (!$tfa_priv_key) $tfa_priv_key = $totp_controller->addPrivateKey($user->ID); $code = $totp_controller->generateOTP($user->ID, $tfa_priv_key); $result = true; } } if ('array' != $response_format) return $result; $ret = array('result' => $result); if ($result) { $ret['user_can_trust'] = $this->user_can_trust($user->ID); if (!empty($params['trust_token']) && $this->user_trust_token_valid($user->ID, $params['trust_token'])) { $ret['user_already_trusted'] = 1; } } return $ret; } /** * Print the radio buttons for enabling/disabling TFA * * @param Integer $user_id - the WordPress user ID * @param Boolean $long_label - whether to use a long label rather than a short one * @param String $style - valid values are "show_current" and "require_current" */ public function paint_enable_tfa_radios($user_id, $long_label = false, $style = 'show_current') { if (!$user_id) return; if ('require_current' != $style) $style = 'show_current'; $is_required = $this->is_required_for_user($user_id); $is_activated = $this->is_activated_by_user($user_id); if ($is_required) { $require_after = absint($this->get_option('tfa_requireafter')); echo '

'.sprintf(__('N.B. This site is configured to forbid you to log in if you disable two-factor authentication after your account is %d days old', 'all-in-one-wp-security-and-firewall'), $require_after).'

'; } $tfa_enabled_label = $long_label ? __('Enable two-factor authentication', 'all-in-one-wp-security-and-firewall') : __('Enabled', 'all-in-one-wp-security-and-firewall'); if ('show_current' == $style) { $tfa_enabled_label .= ' '.sprintf(__('(Current code: %s)', 'all-in-one-wp-security-and-firewall'), $this->get_controller('totp')->current_otp_code($user_id)); } elseif ('require_current' == $style) { $tfa_enabled_label .= ' '.sprintf(__('(you must enter the current code: %s)', 'all-in-one-wp-security-and-firewall'), ''); } $show_disable = ((is_multisite() && is_super_admin()) || (!is_multisite() && current_user_can($this->get_management_capability())) || false == $is_activated || !$is_required || !$this->get_option('tfa_hide_turn_off')) ? true : false; $tfa_disabled_label = $long_label ? __('Disable two-factor authentication', 'all-in-one-wp-security-and-firewall') : __('Disabled', 'all-in-one-wp-security-and-firewall'); if ('require_current' == $style) echo ''."\n"; echo '
'; // Show the 'disabled' option if the user is an admin, or if it is currently set, or if TFA is not compulsory, or if the site owner doesn't require it to be hidden // Note that this just hides the option in the UI. The user could POST to turn off TFA, but, since it's required, they won't be able to log in. if ($show_disable) { echo '
'; } } /** * Retrieve a saved option * * @param String $key - option key * * @return Mixed */ public function get_option($key) { if (!is_multisite()) return get_option($key); $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1; $get_option_site_id = apply_filters('simba_tfa_get_option_site_id', $main_site_id); switch_to_blog($get_option_site_id); $value = get_option($key); restore_current_blog(); return $value; } /** * Updates an option. * * @param String $key - option key * @param Mixed $value - option value * * @return Boolean */ public function update_option($key, $value) { if (!is_multisite()) return update_option($key, $value); $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1; $update_option_site_id = apply_filters('simba_tfa_update_option_site_id', $main_site_id); switch_to_blog($update_option_site_id); $result = update_option($key, $value); restore_current_blog(); return $result; } /** * Deletes an option. * * @param String $key - option key * * @return Boolean */ public function delete_option($key) { if (!is_multisite()) return delete_option($key); $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1; $delete_option_site_id = apply_filters('simba_tfa_delete_option_site_id', $main_site_id); switch_to_blog($delete_option_site_id); $result = delete_option($key); restore_current_blog(); return $result; } /** * Paint a list of checkboxes, one for each role * * @param String $prefix * @param Integer $default - default value (0 or 1) */ public function list_user_roles_checkboxes($prefix = '', $default = 1) { if (is_multisite()) { // Not a real WP role; needs separate handling $id = '_super_admin'; $name = __('Multisite Super Admin', 'all-in-one-wp-security-and-firewall'); $setting = $this->get_option('tfa_'.$prefix.$id); $setting = ($setting === false) ? $default : ($setting ? 1 : 0); echo '
\n"; } global $wp_roles; if (!isset($wp_roles)) $wp_roles = new WP_Roles(); foreach ($wp_roles->role_names as $id => $name) { $setting = $this->get_option('tfa_'.$prefix.$id); $setting = ($setting === false) ? $default : ($setting ? 1 : 0); echo '
\n"; } } public function tfa_list_xmlrpc_status_radios() { $setting = $this->get_option('tfa_xmlrpc_on'); $setting = $setting ? 1 : 0; $types = array( '0' => __('Do not require 2FA over XMLRPC (best option if you must use XMLRPC and your client does not support 2FA)', 'all-in-one-wp-security-and-firewall'), '1' => __('Do require 2FA over XMLRPC (best option if you do not use XMLRPC or are unsure)', 'all-in-one-wp-security-and-firewall') ); foreach($types as $id => $name) { print '
\n"; } } protected function is_caller_active() { if (!defined('XMLRPC_REQUEST') || !XMLRPC_REQUEST) return true; $saved_data = $this->get_option('tfa_xmlrpc_on'); return $saved_data ? true : false; } /** * @param Array $params * @param Boolean $may_be_email * * @return WP_Error|Boolean|Integer - WP_Error or false means failure; true or 1 means success, but true means the TFA code was validated */ public function authorise_user_from_login($params, $may_be_email = false) { $params = apply_filters('simbatfa_auth_user_from_login_params', $params); global $wpdb; if (!$this->is_caller_active()) return 1; $query = ($may_be_email && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) ? $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $params['log']) : $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_login=%s", $params['log']); $response = $wpdb->get_row($query); if (!$response && $may_be_email && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) { // Corner-case: login looks like an email, but is a username rather than email address $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_login=%s", $params['log'])); } $user_id = is_object($response) ? $response->ID : false; $user_registered = is_object($response) ? $response->user_registered : false; $user_code = isset($params['two_factor_code']) ? str_replace(' ', '', trim($params['two_factor_code'])) : ''; // This condition in theory should not be possible if (!$user_id) return new WP_Error('tfa_user_not_found', apply_filters('simbatfa_tfa_user_not_found', ''.__('Error:', 'all-in-one-wp-security-and-firewall').' '.__('The indicated user could not be found.', 'all-in-one-wp-security-and-firewall'))); if (!$this->is_activated_for_user($user_id)) return 1; if (!empty($params['trust_token']) && $this->user_trust_token_valid($user_id, $params['trust_token'])) { return 1; } if (!$this->is_activated_by_user($user_id)) { if (!$this->is_required_for_user($user_id)) return 1; $enforce_require_after_check = true; $require_enforce_after = $this->get_option('tfa_require_enforce_after'); // Don't enforce if the setting has never been saved if (is_string($require_enforce_after) && preg_match('#^(\d+)-(\d+)-(\d+)$#', $require_enforce_after, $enforce_matches)) { // wp_date() is WP 5.3+, but performs translation into the site locale $current_date = function_exists('wp_date') ? wp_date('Y-m-d') : get_date_from_gmt(gmdate('Y-m-d H:i:s'), 'Y-m-d'); if (preg_match('#^(\d+)-(\d+)-(\d+)$#', $current_date, $current_date_matches)) { if ($current_date_matches[0] < $enforce_matches[0] || ($current_date_matches[0] == $enforce_matches[0] && ($current_date_matches[1] < $enforce_matches[1] || ($current_date_matches[1] == $enforce_matches[1] && $current_date_matches[2] < $enforce_matches[2])))) { // Enforcement not yet begun; skip $enforce_require_after_check = false; } } } $require_after = absint($this->get_option('tfa_requireafter')) * 86400; $account_age = time() - strtotime($user_registered); if ($account_age > $require_after && apply_filters('simbatfa_enforce_require_after_check', $enforce_require_after_check, $user_id, $require_after, $account_age)) { return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login', ''.__('Error:', 'all-in-one-wp-security-and-firewall').' '.__('The site owner has forbidden you to login without two-factor authentication. Please contact the site owner to re-gain access.', 'all-in-one-wp-security-and-firewall'))); } return 1; } $tfa_creds_user_id = !empty($params['creds_user_id']) ? $params['creds_user_id'] : $user_id; if ($tfa_creds_user_id != $user_id) { // Authenticating using a different user's credentials (e.g. https://wordpress.org/plugins/use-administrator-password/) // In this case, we require that different user to have TFA active - so that this mechanism can't be used to avoid TFA if (!$this->is_activated_for_user($tfa_creds_user_id) || !$this->is_activated_by_user($tfa_creds_user_id)) { return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login_altuser', ''.__('Error:', 'all-in-one-wp-security-and-firewall').' '.__('You are attempting to log in to an account that has two-factor authentication enabled; this requires you to also have two-factor authentication enabled on the account whose credentials you are using.', 'all-in-one-wp-security-and-firewall'))); } } return $this->get_controller('totp')->check_code_for_user($tfa_creds_user_id, $user_code); } /** * Evaluate whether a trust token is valid for a user * * @param Integer $user_id - WP user ID * @param String $trust_token - trust token * * @return Boolean */ protected function user_trust_token_valid($user_id, $trust_token) { if (!is_string($trust_token) || strlen($trust_token) < 30) return false; $trusted_devices = $this->user_get_trusted_devices($user_id); $time_now = time(); foreach ($trusted_devices as $device) { if (empty($device['until']) || $device['until'] <= $time_now) continue; if (!empty($device['token']) && $device['token'] === $trust_token) { return true; } } return false; } /** * This deals with the issue that wp-login.php does not redirect to a canonical URL. As a result, if a website is available under more than one host, then admin_url('admin-ajax.php') might return a different one than the visitor is using, resulting in AJAX failing due to CORS errors. * * @return String */ protected function get_ajax_url() { $ajax_url = admin_url('admin-ajax.php'); $parsed_url = parse_url($ajax_url); if (strtolower($parsed_url['host']) !== strtolower($_SERVER['HTTP_HOST']) && !empty($parsed_url['path'])) { // Mismatch - return the relative URL only $ajax_url = $parsed_url['path']; } return $ajax_url; } /** * Called not only upon the WP action login_enqueue_scripts, but potentially upon the action 'init' and various others from other plugins too. It can handle being called multiple times. */ public function login_enqueue_scripts() { if (!$this->should_enqueue_login_scripts()) { return; } if (isset($_GET['action']) && 'logout ' != $_GET['action'] && 'login' != $_GET['action']) return; static $already_done = false; if ($already_done) return; $already_done = true; // Prevent caching when in debug mode $script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/tfa.js'); wp_enqueue_script('tfa-ajax-request', $this->includes_url().'/tfa.js', array('jquery'), $script_ver); $trusted_for = $this->get_option('tfa_trusted_for'); $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for); $localize = array( 'ajaxurl' => $this->get_ajax_url(), 'click_to_enter_otp' => __("Click to enter One Time Password", 'all-in-one-wp-security-and-firewall'), 'enter_username_first' => __('You have to enter a username first.', 'all-in-one-wp-security-and-firewall'), 'otp' => __('One Time Password (i.e. 2FA)', 'all-in-one-wp-security-and-firewall'), 'otp_login_help' => __('(check your OTP app to get this password)', 'all-in-one-wp-security-and-firewall'), 'mark_as_trusted' => sprintf(_n('Trust this device (allow login without 2FA for %d day)', 'Trust this device (allow login without TFA for %d days)', $trusted_for, 'all-in-one-wp-security-and-firewall'), $trusted_for), 'is_trusted' => __('(Trusted device - no OTP code required)', 'all-in-one-wp-security-and-firewall'), 'nonce' => wp_create_nonce('simba_tfa_loginform_nonce'), 'login_form_selectors' => '', 'login_form_off_selectors' => '', 'error' => __('An error has occurred. Site owners can check the JavaScript console for more details.', 'all-in-one-wp-security-and-firewall'), ); // Spinner exists since WC 3.8. Use the proper functions to avoid SSL warnings. if (file_exists(ABSPATH.'wp-admin/images/spinner-2x.gif')) { $localize['spinnerimg'] = admin_url('images/spinner-2x.gif'); } elseif (file_exists(ABSPATH.WPINC.'/images/spinner-2x.gif')) { $localize['spinnerimg'] = includes_url('images/spinner-2x.gif'); } $localize = apply_filters('simba_tfa_login_enqueue_localize', $localize); wp_localize_script('tfa-ajax-request', 'simba_tfasettings', $localize); } /** * Check whether TFA login scripts should be enqueued or not. * * @return boolean True if the TFA login script should be enqueued, otherwise false. */ private function should_enqueue_login_scripts() { if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) { return apply_filters('simbatfa_enqueue_login_scripts', false); } global $wpdb; $sql = $wpdb->prepare('SELECT COUNT(user_id) FROM ' . $wpdb->usermeta . ' WHERE meta_key = %s AND meta_value = %d LIMIT 1', 'tfa_enable_tfa', 1); $count_user_id = $wpdb->get_var($sql); if (is_null($count_user_id)) { // Error in query. return apply_filters('simbatfa_enqueue_login_scripts', true); } elseif ($count_user_id > 0) { // A user exists with TFA enabled. return apply_filters('simbatfa_enqueue_login_scripts', true); } // No user exists with TFA enabled. return apply_filters('simbatfa_enqueue_login_scripts', false); } /** * Return or output view content * * @param String $path - path to template, usually relative to templates/ within the plugin directory * @param Array $extract_these - key/value pairs for substitution into the scope of the template * @param Boolean $return_instead_of_echo - what to do with the results * * @return String|Void */ public function include_template($path, $extract_these = array(), $return_instead_of_echo = false) { if ($return_instead_of_echo) ob_start(); $template_file = apply_filters('simatfa_template_file', $this->templates_dir().'/'.$path, $path, $extract_these, $return_instead_of_echo); do_action('simbatfa_before_template', $path, $return_instead_of_echo, $extract_these, $template_file); if (!file_exists($template_file)) { error_log("TFA: template not found: $template_file (from $path)"); echo __('Error:', 'all-in-one-wp-security-and-firewall').' '.__('Template path not found:', 'all-in-one-wp-security-and-firewall')." (".htmlspecialchars($path).")"; } else { extract($extract_these); // The following are useful variables which can be used in the template. // They appear as unused, but may be used in the $template_file. $wpdb = $GLOBALS['wpdb'];// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wpdb might be used in the included template $simba_tfa = $this;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template $totp_controller = $this->get_controller('totp');// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template include $template_file; } do_action('simbatfa_after_template', $path, $return_instead_of_echo, $extract_these, $template_file); if ($return_instead_of_echo) return ob_get_clean(); } /** * Make sure that self::$frontend is the instance of Simba_TFA_Frontend, and return it * * @return Simba_TFA_Frontend */ public function load_frontend() { if (!class_exists('Simba_TFA_Frontend')) require_once($this->includes_dir().'/tfa_frontend.php'); if (empty($this->frontend)) $this->frontend = new Simba_TFA_Frontend($this); return $this->frontend; } // __return_empty_string() does not exist until WP 3.7 public function shortcode_when_not_logged_in() { return ''; } /** * Set authentication slug. * * @param String $authentication_slug - Authentication slug. Verify that two-factor authentication should not be repeated for the same slug. */ public function set_authentication_slug($authentication_slug) { $this->authentication_slug = $authentication_slug; } }