rr Array of parsed post data. * @param array $postarr Array of sanitized, but otherwise unmodified post data. */ $post_parent = apply_filters( 'wp_insert_post_parent', $post_parent, $post_id, $new_postarr, $postarr ); /* * If the post is being untrashed and it has a desired slug stored in post meta, * reassign it. */ if ( 'trash' === $previous_status && 'trash' !== $post_status ) { $desired_post_slug = get_post_meta( $post_id, '_wp_desired_post_slug', true ); if ( $desired_post_slug ) { delete_post_meta( $post_id, '_wp_desired_post_slug' ); $post_name = $desired_post_slug; } } // If a trashed post has the desired slug, change it and let this post have it. if ( 'trash' !== $post_status && $post_name ) { /** * Filters whether or not to add a `__trashed` suffix to trashed posts that match the name of the updated post. * * @since 5.4.0 * * @param bool $add_trashed_suffix Whether to attempt to add the suffix. * @param string $post_name The name of the post being updated. * @param int $post_id Post ID. */ $add_trashed_suffix = apply_filters( 'add_trashed_suffix_to_trashed_posts', true, $post_name, $post_id ); if ( $add_trashed_suffix ) { wp_add_trashed_suffix_to_post_name_for_trashed_posts( $post_name, $post_id ); } } // When trashing an existing post, change its slug to allow non-trashed posts to use it. if ( 'trash' === $post_status && 'trash' !== $previous_status && 'new' !== $previous_status ) { $post_name = wp_add_trashed_suffix_to_post_name_for_post( $post_id ); } $post_name = wp_unique_post_slug( $post_name, $post_id, $post_status, $post_type, $post_parent ); // Don't unslash. $post_mime_type = isset( $postarr['post_mime_type'] ) ? $postarr['post_mime_type'] : ''; // Expected_slashed (everything!). $data = compact( 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_content_filtered', 'post_title', 'post_excerpt', 'post_status', 'post_type', 'comment_status', 'ping_status', 'post_password', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt', 'post_parent', 'menu_order', 'post_mime_type', 'guid' ); $emoji_fields = array( 'post_title', 'post_content', 'post_excerpt' ); foreach ( $emoji_fields as $emoji_field ) { if ( isset( $data[ $emoji_field ] ) ) { $charset = $wpdb->get_col_charset( $wpdb->posts, $emoji_field ); if ( 'utf8' === $charset ) { $data[ $emoji_field ] = wp_encode_emoji( $data[ $emoji_field ] ); } } } if ( 'attachment' === $post_type ) { /** * Filters attachment post data before it is updated in or added to the database. * * @since 3.9.0 * @since 5.4.1 The `$unsanitized_postarr` parameter was added. * @since 6.0.0 The `$update` parameter was added. * * @param array $data An array of slashed, sanitized, and processed attachment post data. * @param array $postarr An array of slashed and sanitized attachment post data, but not processed. * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data * as originally passed to wp_insert_post(). * @param bool $update Whether this is an existing attachment post being updated. */ $data = apply_filters( 'wp_insert_attachment_data', $data, $postarr, $unsanitized_postarr, $update ); } else { /** * Filters slashed post data just before it is inserted into the database. * * @since 2.7.0 * @since 5.4.1 The `$unsanitized_postarr` parameter was added. * @since 6.0.0 The `$update` parameter was added. * * @param array $data An array of slashed, sanitized, and processed post data. * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data. * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed post data as * originally passed to wp_insert_post(). * @param bool $update Whether this is an existing post being updated. */ $data = apply_filters( 'wp_insert_post_data', $data, $postarr, $unsanitized_postarr, $update ); } $data = wp_unslash( $data ); $where = array( 'ID' => $post_id ); if ( $update ) { /** * Fires immediately before an existing post is updated in the database. * * @since 2.5.0 * * @param int $post_id Post ID. * @param array $data Array of unslashed post data. */ do_action( 'pre_post_update', $post_id, $data ); if ( false === $wpdb->update( $wpdb->posts, $data, $where ) ) { if ( $wp_error ) { if ( 'attachment' === $post_type ) { $message = __( 'Could not update attachment in the database.' ); } else { $message = __( 'Could not update post in the database.' ); } return new WP_Error( 'db_update_error', $message, $wpdb->last_error ); } else { return 0; } } } else { // If there is a suggested ID, use it if not already present. if ( ! empty( $import_id ) ) { $import_id = (int) $import_id; if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE ID = %d", $import_id ) ) ) { $data['ID'] = $import_id; } } if ( false === $wpdb->insert( $wpdb->posts, $data ) ) { if ( $wp_error ) { if ( 'attachment' === $post_type ) { $message = __( 'Could not insert attachment into the database.' ); } else { $message = __( 'Could not insert post into the database.' ); } return new WP_Error( 'db_insert_error', $message, $wpdb->last_error ); } else { return 0; } } $post_id = (int) $wpdb->insert_id; // Use the newly generated $post_id. $where = array( 'ID' => $post_id ); } if ( empty( $data['post_name'] ) && ! in_array( $data['post_status'], array( 'draft', 'pending', 'auto-draft' ), true ) ) { $data['post_name'] = wp_unique_post_slug( sanitize_title( $data['post_title'], $post_id ), $post_id, $data['post_status'], $post_type, $post_parent ); $wpdb->update( $wpdb->posts, array( 'post_name' => $data['post_name'] ), $where ); clean_post_cache( $post_id ); } if ( is_object_in_taxonomy( $post_type, 'category' ) ) { wp_set_post_categories( $post_id, $post_category ); } if ( isset( $postarr['tags_input'] ) && is_object_in_taxonomy( $post_type, 'post_tag' ) ) { wp_set_post_tags( $post_id, $postarr['tags_input'] ); } // Add default term for all associated custom taxonomies. if ( 'auto-draft' !== $post_status ) { foreach ( get_object_taxonomies( $post_type, 'object' ) as $taxonomy => $tax_object ) { if ( ! empty( $tax_object->default_term ) ) { // Filter out empty terms. if ( isset( $postarr['tax_input'][ $taxonomy ] ) && is_array( $postarr['tax_input'][ $taxonomy ] ) ) { $postarr['tax_input'][ $taxonomy ] = array_filter( $postarr['tax_input'][ $taxonomy ] ); } // Passed custom taxonomy list overwrites the existing list if not empty. $terms = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) ); if ( ! empty( $terms ) && empty( $postarr['tax_input'][ $taxonomy ] ) ) { $postarr['tax_input'][ $taxonomy ] = $terms; } if ( empty( $postarr['tax_input'][ $taxonomy ] ) ) { $default_term_id = get_option( 'default_term_' . $taxonomy ); if ( ! empty( $default_term_id ) ) { $postarr['tax_input'][ $taxonomy ] = array( (int) $default_term_id ); } } } } } // New-style support for all custom taxonomies. if ( ! empty( $postarr['tax_input'] ) ) { foreach ( $postarr['tax_input'] as $taxonomy => $tags ) { $taxonomy_obj = get_taxonomy( $taxonomy ); if ( ! $taxonomy_obj ) { /* translators: %s: Taxonomy name. */ _doing_it_wrong( __FUNCTION__, sprintf( __( 'Invalid taxonomy: %s.' ), $taxonomy ), '4.4.0' ); continue; } // array = hierarchical, string = non-hierarchical. if ( is_array( $tags ) ) { $tags = array_filter( $tags ); } if ( current_user_can( $taxonomy_obj->cap->assign_terms ) ) { wp_set_post_terms( $post_id, $tags, $taxonomy ); } } } if ( ! empty( $postarr['meta_input'] ) ) { foreach ( $postarr['meta_input'] as $field => $value ) { update_post_meta( $post_id, $field, $value ); } } $current_guid = get_post_field( 'guid', $post_id ); // Set GUID. if ( ! $update && '' === $current_guid ) { $wpdb->update( $wpdb->posts, array( 'guid' => get_permalink( $post_id ) ), $where ); } if ( 'attachment' === $postarr['post_type'] ) { if ( ! empty( $postarr['file'] ) ) { update_attached_file( $post_id, $postarr['file'] ); } if ( ! empty( $postarr['context'] ) ) { add_post_meta( $post_id, '_wp_attachment_context', $postarr['context'], true ); } } // Set or remove featured image. if ( isset( $postarr['_thumbnail_id'] ) ) { $thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' ) || 'revision' === $post_type; if ( ! $thumbnail_support && 'attachment' === $post_type && $post_mime_type ) { if ( wp_attachment_is( 'audio', $post_id ) ) { $thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' ); } elseif ( wp_attachment_is( 'video', $post_id ) ) { $thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' ); } } if ( $thumbnail_support ) { $thumbnail_id = (int) $postarr['_thumbnail_id']; if ( -1 === $thumbnail_id ) { delete_post_thumbnail( $post_id ); } else { set_post_thumbnail( $post_id, $thumbnail_id ); } } } clean_post_cache( $post_id ); $post = get_post( $post_id ); if ( ! empty( $postarr['page_template'] ) ) { $post->page_template = $postarr['page_template']; $page_templates = wp_get_theme()->get_page_templates( $post ); if ( 'default' !== $postarr['page_template'] && ! isset( $page_templates[ $postarr['page_template'] ] ) ) { if ( $wp_error ) { return new WP_Error( 'invalid_page_template', __( 'Invalid page template.' ) ); } update_post_meta( $post_id, '_wp_page_template', 'default' ); } else { update_post_meta( $post_id, '_wp_page_template', $postarr['page_template'] ); } } if ( 'attachment' !== $postarr['post_type'] ) { wp_transition_post_status( $data['post_status'], $previous_status, $post ); } else { if ( $update ) { /** * Fires once an existing attachment has been updated. * * @since 2.0.0 * * @param int $post_id Attachment ID. */ do_action( 'edit_attachment', $post_id ); $post_after = get_post( $post_id ); /** * Fires once an existing attachment has been updated. * * @since 4.4.0 * * @param int $post_id Post ID. * @param WP_Post $post_after Post object following the update. * @param WP_Post $post_before Post object before the update. */ do_action( 'attachment_updated', $post_id, $post_after, $post_before ); } else { /** * Fires once an attachment has been added. * * @since 2.0.0 * * @param int $post_id Attachment ID. */ do_action( 'add_attachment', $post_id ); } return $post_id; } if ( $update ) { /** * Fires once an existing post has been updated. * * The dynamic portion of the hook name, `$post->post_type`, refers to * the post type slug. * * Possible hook names include: * * - `edit_post_post` * - `edit_post_page` * * @since 5.1.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ do_action( "edit_post_{$post->post_type}", $post_id, $post ); /** * Fires once an existing post has been updated. * * @since 1.2.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ do_action( 'edit_post', $post_id, $post ); $post_after = get_post( $post_id ); /** * Fires once an existing post has been updated. * * @since 3.0.0 * * @param int $post_id Post ID. * @param WP_Post $post_after Post object following the update. * @param WP_Post $post_before Post object before the update. */ do_action( 'post_updated', $post_id, $post_after, $post_before ); } /** * Fires once a post has been saved. * * The dynamic portion of the hook name, `$post->post_type`, refers to * the post type slug. * * Possible hook names include: * * - `save_post_post` * - `save_post_page` * * @since 3.7.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. */ do_action( "save_post_{$post->post_type}", $post_id, $post, $update ); /** * Fires once a post has been saved. * * @since 1.5.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. */ do_action( 'save_post', $post_id, $post, $update ); /** * Fires once a post has been saved. * * @since 2.0.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. */ do_action( 'wp_insert_post', $post_id, $post, $update ); if ( $fire_after_hooks ) { wp_after_insert_post( $post, $update, $post_before ); } return $post_id; } /** * Updates a post with new post data. * * The date does not have to be set for drafts. You can set the date and it will * not be overridden. * * @since 1.0.0 * @since 3.5.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure. * @since 5.6.0 Added the `$fire_after_hooks` parameter. * * @param array|object $postarr Optional. Post data. Arrays are expected to be escaped, * objects are not. See wp_insert_post() for accepted arguments. * Default array. * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true. * @return int|WP_Error The post ID on success. The value 0 or WP_Error on failure. */ function wp_update_post( $postarr = array(), $wp_error = false, $fire_after_hooks = true ) { if ( is_object( $postarr ) ) { // Non-escaped post was passed. $postarr = get_object_vars( $postarr ); $postarr = wp_slash( $postarr ); } // First, get all of the original fields. $post = get_post( $postarr['ID'], ARRAY_A ); if ( is_null( $post ) ) { if ( $wp_error ) { return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) ); } return 0; } // Escape data pulled from DB. $post = wp_slash( $post ); // Passed post category list overwrites existing category list if not empty. if ( isset( $postarr['post_category'] ) && is_array( $postarr['post_category'] ) && count( $postarr['post_category'] ) > 0 ) { $post_cats = $postarr['post_category']; } else { $post_cats = $post['post_category']; } // Drafts shouldn't be assigned a date unless explicitly done so by the user. if ( isset( $post['post_status'] ) && in_array( $post['post_status'], array( 'draft', 'pending', 'auto-draft' ), true ) && empty( $postarr['edit_date'] ) && ( '0000-00-00 00:00:00' === $post['post_date_gmt'] ) ) { $clear_date = true; } else { $clear_date = false; } // Merge old and new fields with new fields overwriting old ones. $postarr = array_merge( $post, $postarr ); $postarr['post_category'] = $post_cats; if ( $clear_date ) { $postarr['post_date'] = current_time( 'mysql' ); $postarr['post_date_gmt'] = ''; } if ( 'attachment' === $postarr['post_type'] ) { return wp_insert_attachment( $postarr, false, 0, $wp_error ); } // Discard 'tags_input' parameter if it's the same as existing post tags. if ( isset( $postarr['tags_input'] ) && is_object_in_taxonomy( $postarr['post_type'], 'post_tag' ) ) { $tags = get_the_terms( $postarr['ID'], 'post_tag' ); $tag_names = array(); if ( $tags && ! is_wp_error( $tags ) ) { $tag_names = wp_list_pluck( $tags, 'name' ); } if ( $postarr['tags_input'] === $tag_names ) { unset( $postarr['tags_input'] ); } } return wp_insert_post( $postarr, $wp_error, $fire_after_hooks ); } /** * Publishes a post by transitioning the post status. * * @since 2.1.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|WP_Post $post Post ID or post object. */ function wp_publish_post( $post ) { global $wpdb; $post = get_post( $post ); if ( ! $post ) { return; } if ( 'publish' === $post->post_status ) { return; } $post_before = get_post( $post->ID ); // Ensure at least one term is applied for taxonomies with a default term. foreach ( get_object_taxonomies( $post->post_type, 'object' ) as $taxonomy => $tax_object ) { // Skip taxonomy if no default term is set. if ( 'category' !== $taxonomy && empty( $tax_object->default_term ) ) { continue; } // Do not modify previously set terms. if ( ! empty( get_the_terms( $post, $taxonomy ) ) ) { continue; } if ( 'category' === $taxonomy ) { $default_term_id = (int) get_option( 'default_category', 0 ); } else { $default_term_id = (int) get_option( 'default_term_' . $taxonomy, 0 ); } if ( ! $default_term_id ) { continue; } wp_set_post_terms( $post->ID, array( $default_term_id ), $taxonomy ); } $wpdb->update( $wpdb->posts, array( 'post_status' => 'publish' ), array( 'ID' => $post->ID ) ); clean_post_cache( $post->ID ); $old_status = $post->post_status; $post->post_status = 'publish'; wp_transition_post_status( 'publish', $old_status, $post ); /** This action is documented in wp-includes/post.php */ do_action( "edit_post_{$post->post_type}", $post->ID, $post ); /** This action is documented in wp-includes/post.php */ do_action( 'edit_post', $post->ID, $post ); /** This action is documented in wp-includes/post.php */ do_action( "save_post_{$post->post_type}", $post->ID, $post, true ); /** This action is documented in wp-includes/post.php */ do_action( 'save_post', $post->ID, $post, true ); /** This action is documented in wp-includes/post.php */ do_action( 'wp_insert_post', $post->ID, $post, true ); wp_after_insert_post( $post, true, $post_before ); } /** * Publishes future post and make sure post ID has future post status. * * Invoked by cron 'publish_future_post' event. This safeguard prevents cron * from publishing drafts, etc. * * @since 2.5.0 * * @param int|WP_Post $post Post ID or post object. */ function check_and_publish_future_post( $post ) { $post = get_post( $post ); if ( ! $post ) { return; } if ( 'future' !== $post->post_status ) { return; } $time = strtotime( $post->post_date_gmt . ' GMT' ); // Uh oh, someone jumped the gun! if ( $time > time() ) { wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); // Clear anything else in the system. wp_schedule_single_event( $time, 'publish_future_post', array( $post->ID ) ); return; } // wp_publish_post() returns no meaningful value. wp_publish_post( $post->ID ); } /** * Uses wp_checkdate to return a valid Gregorian-calendar value for post_date. * If post_date is not provided, this first checks post_date_gmt if provided, * then falls back to use the current time. * * For back-compat purposes in wp_insert_post, an empty post_date and an invalid * post_date_gmt will continue to return '1970-01-01 00:00:00' rather than false. * * @since 5.7.0 * * @param string $post_date The date in mysql format (`Y-m-d H:i:s`). * @param string $post_date_gmt The GMT date in mysql format (`Y-m-d H:i:s`). * @return string|false A valid Gregorian-calendar date string, or false on failure. */ function wp_resolve_post_date( $post_date = '', $post_date_gmt = '' ) { // If the date is empty, set the date to now. if ( empty( $post_date ) || '0000-00-00 00:00:00' === $post_date ) { if ( empty( $post_date_gmt ) || '0000-00-00 00:00:00' === $post_date_gmt ) { $post_date = current_time( 'mysql' ); } else { $post_date = get_date_from_gmt( $post_date_gmt ); } } // Validate the date. $month = (int) substr( $post_date, 5, 2 ); $day = (int) substr( $post_date, 8, 2 ); $year = (int) substr( $post_date, 0, 4 ); $valid_date = wp_checkdate( $month, $day, $year, $post_date ); if ( ! $valid_date ) { return false; } return $post_date; } /** * Computes a unique slug for the post, when given the desired slug and some post details. * * @since 2.8.0 * * @global wpdb $wpdb WordPress database abstraction object. * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $slug The desired slug (post_name). * @param int $post_id Post ID. * @param string $post_status No uniqueness checks are made if the post is still draft or pending. * @param string $post_type Post type. * @param int $post_parent Post parent ID. * @return string Unique slug for the post, based on $post_name (with a -1, -2, etc. suffix) */ function wp_unique_post_slug( $slug, $post_id, $post_status, $post_type, $post_parent ) { if ( in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ), true ) || ( 'inherit' === $post_status && 'revision' === $post_type ) || 'user_request' === $post_type ) { return $slug; } /** * Filters the post slug before it is generated to be unique. * * Returning a non-null value will short-circuit the * unique slug generation, returning the passed value instead. * * @since 5.1.0 * * @param string|null $override_slug Short-circuit return value. * @param string $slug The desired slug (post_name). * @param int $post_id Post ID. * @param string $post_status The post status. * @param string $post_type Post type. * @param int $post_parent Post parent ID. */ $override_slug = apply_filters( 'pre_wp_unique_post_slug', null, $slug, $post_id, $post_status, $post_type, $post_parent ); if ( null !== $override_slug ) { return $override_slug; } global $wpdb, $wp_rewrite; $original_slug = $slug; $feeds = $wp_rewrite->feeds; if ( ! is_array( $feeds ) ) { $feeds = array(); } if ( 'attachment' === $post_type ) { // Attachment slugs must be unique across all types. $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND ID != %d LIMIT 1"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_id ) ); /** * Filters whether the post slug would make a bad attachment slug. * * @since 3.1.0 * * @param bool $bad_slug Whether the slug would be bad as an attachment slug. * @param string $slug The post slug. */ $is_bad_attachment_slug = apply_filters( 'wp_unique_post_slug_is_bad_attachment_slug', false, $slug ); if ( $post_name_check || in_array( $slug, $feeds, true ) || 'embed' === $slug || $is_bad_attachment_slug ) { $suffix = 2; do { $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_id ) ); ++$suffix; } while ( $post_name_check ); $slug = $alt_post_name; } } elseif ( is_post_type_hierarchical( $post_type ) ) { if ( 'nav_menu_item' === $post_type ) { return $slug; } /* * Page slugs must be unique within their own trees. Pages are in a separate * namespace than posts so page slugs are allowed to overlap post slugs. */ $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type IN ( %s, 'attachment' ) AND ID != %d AND post_parent = %d LIMIT 1"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_id, $post_parent ) ); /** * Filters whether the post slug would make a bad hierarchical post slug. * * @since 3.1.0 * * @param bool $bad_slug Whether the post slug would be bad in a hierarchical post context. * @param string $slug The post slug. * @param string $post_type Post type. * @param int $post_parent Post parent ID. */ $is_bad_hierarchical_slug = apply_filters( 'wp_unique_post_slug_is_bad_hierarchical_slug', false, $slug, $post_type, $post_parent ); if ( $post_name_check || in_array( $slug, $feeds, true ) || 'embed' === $slug || preg_match( "@^($wp_rewrite->pagination_base)?\d+$@", $slug ) || $is_bad_hierarchical_slug ) { $suffix = 2; do { $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_id, $post_parent ) ); ++$suffix; } while ( $post_name_check ); $slug = $alt_post_name; } } else { // Post slugs must be unique across all posts. $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_id ) ); $post = get_post( $post_id ); // Prevent new post slugs that could result in URLs that conflict with date archives. $conflicts_with_date_archive = false; if ( 'post' === $post_type && ( ! $post || $post->post_name !== $slug ) && preg_match( '/^[0-9]+$/', $slug ) ) { $slug_num = (int) $slug; if ( $slug_num ) { $permastructs = array_values( array_filter( explode( '/', get_option( 'permalink_structure' ) ) ) ); $postname_index = array_search( '%postname%', $permastructs, true ); /* * Potential date clashes are as follows: * * - Any integer in the first permastruct position could be a year. * - An integer between 1 and 12 that follows 'year' conflicts with 'monthnum'. * - An integer between 1 and 31 that follows 'monthnum' conflicts with 'day'. */ if ( 0 === $postname_index || ( $postname_index && '%year%' === $permastructs[ $postname_index - 1 ] && 13 > $slug_num ) || ( $postname_index && '%monthnum%' === $permastructs[ $postname_index - 1 ] && 32 > $slug_num ) ) { $conflicts_with_date_archive = true; } } } /** * Filters whether the post slug would be bad as a flat slug. * * @since 3.1.0 * * @param bool $bad_slug Whether the post slug would be bad as a flat slug. * @param string $slug The post slug. * @param string $post_type Post type. */ $is_bad_flat_slug = apply_filters( 'wp_unique_post_slug_is_bad_flat_slug', false, $slug, $post_type ); if ( $post_name_check || in_array( $slug, $feeds, true ) || 'embed' === $slug || $conflicts_with_date_archive || $is_bad_flat_slug ) { $suffix = 2; do { $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_id ) ); ++$suffix; } while ( $post_name_check ); $slug = $alt_post_name; } } /** * Filters the unique post slug. * * @since 3.3.0 * * @param string $slug The post slug. * @param int $post_id Post ID. * @param string $post_status The post status. * @param string $post_type Post type. * @param int $post_parent Post parent ID * @param string $original_slug The original post slug. */ return apply_filters( 'wp_unique_post_slug', $slug, $post_id, $post_status, $post_type, $post_parent, $original_slug ); } /** * Truncates a post slug. * * @since 3.6.0 * @access private * * @see utf8_uri_encode() * * @param string $slug The slug to truncate. * @param int $length Optional. Max length of the slug. Default 200 (characters). * @return string The truncated slug. */ function _truncate_post_slug( $slug, $length = 200 ) { if ( strlen( $slug ) > $length ) { $decoded_slug = urldecode( $slug ); if ( $decoded_slug === $slug ) { $slug = substr( $slug, 0, $length ); } else { $slug = utf8_uri_encode( $decoded_slug, $length, true ); } } return rtrim( $slug, '-' ); } /** * Adds tags to a post. * * @see wp_set_post_tags() * * @since 2.3.0 * * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post. * @param string|array $tags Optional. An array of tags to set for the post, or a string of tags * separated by commas. Default empty. * @return array|false|WP_Error Array of affected term IDs. WP_Error or false on failure. */ function wp_add_post_tags( $post_id = 0, $tags = '' ) { return wp_set_post_tags( $post_id, $tags, true ); } /** * Sets the tags for a post. * * @since 2.3.0 * * @see wp_set_object_terms() * * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post. * @param string|array $tags Optional. An array of tags to set for the post, or a string of tags * separated by commas. Default empty. * @param bool $append Optional. If true, don't delete existing tags, just add on. If false, * replace the tags with the new tags. Default false. * @return array|false|WP_Error Array of term taxonomy IDs of affected terms. WP_Error or false on failure. */ function wp_set_post_tags( $post_id = 0, $tags = '', $append = false ) { return wp_set_post_terms( $post_id, $tags, 'post_tag', $append ); } /** * Sets the terms for a post. * * @since 2.8.0 * * @see wp_set_object_terms() * * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post. * @param string|array $terms Optional. An array of terms to set for the post, or a string of terms * separated by commas. Hierarchical taxonomies must always pass IDs rather * than names so that children with the same names but different parents * aren't confused. Default empty. * @param string $taxonomy Optional. Taxonomy name. Default 'post_tag'. * @param bool $append Optional. If true, don't delete existing terms, just add on. If false, * replace the terms with the new terms. Default false. * @return array|false|WP_Error Array of term taxonomy IDs of affected terms. WP_Error or false on failure. */ function wp_set_post_terms( $post_id = 0, $terms = '', $taxonomy = 'post_tag', $append = false ) { $post_id = (int) $post_id; if ( ! $post_id ) { return false; } if ( empty( $terms ) ) { $terms = array(); } if ( ! is_array( $terms ) ) { $comma = _x( ',', 'tag delimiter' ); if ( ',' !== $comma ) { $terms = str_replace( $comma, ',', $terms ); } $terms = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) ); } /* * Hierarchical taxonomies must always pass IDs rather than names so that * children with the same names but different parents aren't confused. */ if ( is_taxonomy_hierarchical( $taxonomy ) ) { $terms = array_unique( array_map( 'intval', $terms ) ); } return wp_set_object_terms( $post_id, $terms, $taxonomy, $append ); } /** * Sets categories for a post. * * If no categories are provided, the default category is used. * * @since 2.1.0 * * @param int $post_id Optional. The Post ID. Does not default to the ID * of the global $post. Default 0. * @param int[]|int $post_categories Optional. List of category IDs, or the ID of a single category. * Default empty array. * @param bool $append If true, don't delete existing categories, just add on. * If false, replace the categories with the new categories. * @return array|false|WP_Error Array of term taxonomy IDs of affected categories. WP_Error or false on failure. */ function wp_set_post_categories( $post_id = 0, $post_categories = array(), $append = false ) { $post_id = (int) $post_id; $post_type = get_post_type( $post_id ); $post_status = get_post_status( $post_id ); // If $post_categories isn't already an array, make it one. $post_categories = (array) $post_categories; if ( empty( $post_categories ) ) { /** * Filters post types (in addition to 'post') that require a default category. * * @since 5.5.0 * * @param string[] $post_types An array of post type names. Default empty array. */ $default_category_post_types = apply_filters( 'default_category_post_types', array() ); // Regular posts always require a default category. $default_category_post_types = array_merge( $default_category_post_types, array( 'post' ) ); if ( in_array( $post_type, $default_category_post_types, true ) && is_object_in_taxonomy( $post_type, 'category' ) && 'auto-draft' !== $post_status ) { $post_categories = array( get_option( 'default_category' ) ); $append = false; } else { $post_categories = array(); } } elseif ( 1 === count( $post_categories ) && '' === reset( $post_categories ) ) { return true; } return wp_set_post_terms( $post_id, $post_categories, 'category', $append ); } /** * Fires actions related to the transitioning of a post's status. * * When a post is saved, the post status is "transitioned" from one status to another, * though this does not always mean the status has actually changed before and after * the save. This function fires a number of action hooks related to that transition: * the generic {@see 'transition_post_status'} action, as well as the dynamic hooks * {@see '$old_status_to_$new_status'} and {@see '$new_status_$post->post_type'}. Note * that the function does not transition the post object in the database. * * For instance: When publishing a post for the first time, the post status may transition * from 'draft' – or some other status – to 'publish'. However, if a post is already * published and is simply being updated, the "old" and "new" statuses may both be 'publish' * before and after the transition. * * @since 2.3.0 * * @param string $new_status Transition to this post status. * @param string $old_status Previous post status. * @param WP_Post $post Post data. */ function wp_transition_post_status( $new_status, $old_status, $post ) { /** * Fires when a post is transitioned from one status to another. * * @since 2.3.0 * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. */ do_action( 'transition_post_status', $new_status, $old_status, $post ); /** * Fires when a post is transitioned from one status to another. * * The dynamic portions of the hook name, `$new_status` and `$old_status`, * refer to the old and new post statuses, respectively. * * Possible hook names include: * * - `draft_to_publish` * - `publish_to_trash` * - `pending_to_draft` * * @since 2.3.0 * * @param WP_Post $post Post object. */ do_action( "{$old_status}_to_{$new_status}", $post ); /** * Fires when a post is transitioned from one status to another. * * The dynamic portions of the hook name, `$new_status` and `$post->post_type`, * refer to the new post status and post type, respectively. * * Possible hook names include: * * - `draft_post` * - `future_post` * - `pending_post` * - `private_post` * - `publish_post` * - `trash_post` * - `draft_page` * - `future_page` * - `pending_page` * - `private_page` * - `publish_page` * - `trash_page` * - `publish_attachment` * - `trash_attachment` * * Please note: When this action is hooked using a particular post status (like * 'publish', as `publish_{$post->post_type}`), it will fire both when a post is * first transitioned to that status from something else, as well as upon * subsequent post updates (old and new status are both the same). * * Therefore, if you are looking to only fire a callback when a post is first * transitioned to a status, use the {@see 'transition_post_status'} hook instead. * * @since 2.3.0 * @since 5.9.0 Added `$old_status` parameter. * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param string $old_status Old post status. */ do_action( "{$new_status}_{$post->post_type}", $post->ID, $post, $old_status ); } /** * Fires actions after a post, its terms and meta data has been saved. * * @since 5.6.0 * * @param int|WP_Post $post The post ID or object that has been saved. * @param bool $update Whether this is an existing post being updated. * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior * to the update for updated posts. */ function wp_after_insert_post( $post, $update, $post_before ) { $post = get_post( $post ); if ( ! $post ) { return; } $post_id = $post->ID; /** * Fires once a post, its terms and meta data has been saved. * * @since 5.6.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior * to the update for updated posts. */ do_action( 'wp_after_insert_post', $post_id, $post, $update, $post_before ); } // // Comment, trackback, and pingback functions. // /** * Adds a URL to those already pinged. * * @since 1.5.0 * @since 4.7.0 `$post` can be a WP_Post object. * @since 4.7.0 `$uri` can be an array of URIs. * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|WP_Post $post Post ID or post object. * @param string|array $uri Ping URI or array of URIs. * @return int|false How many rows were updated. */ function add_ping( $post, $uri ) { global $wpdb; $post = get_post( $post ); if ( ! $post ) { return false; } $pung = trim( $post->pinged ); $pung = preg_split( '/\s/', $pung ); if ( is_array( $uri ) ) { $pung = array_merge( $pung, $uri ); } else { $pung[] = $uri; } $new = implode( "\n", $pung ); /** * Filters the new ping URL to add for the given post. * * @since 2.0.0 * * @param string $new New ping URL to add. */ $new = apply_filters( 'add_ping', $new ); $return = $wpdb->update( $wpdb->posts, array( 'pinged' => $new ), array( 'ID' => $post->ID ) ); clean_post_cache( $post->ID ); return $return; } /** * Retrieves enclosures already enclosed for a post. * * @since 1.5.0 * * @param int $post_id Post ID. * @return string[] Array of enclosures for the given post. */ function get_enclosed( $post_id ) { $custom_fields = get_post_custom( $post_id ); $pung = array(); if ( ! is_array( $custom_fields ) ) { return $pung; } foreach ( $custom_fields as $key => $val ) { if ( 'enclosure' !== $key || ! is_array( $val ) ) { continue; } foreach ( $val as $enc ) { $enclosure = explode( "\n", $enc ); $pung[] = trim( $enclosure[0] ); } } /** * Filters the list of enclosures already enclosed for the given post. * * @since 2.0.0 * * @param string[] $pung Array of enclosures for the given post. * @param int $post_id Post ID. */ return apply_filters( 'get_enclosed', $pung, $post_id ); } /** * Retrieves URLs already pinged for a post. * * @since 1.5.0 * * @since 4.7.0 `$post` can be a WP_Post object. * * @param int|WP_Post $post Post ID or object. * @return string[]|false Array of URLs already pinged for the given post, false if the post is not found. */ function get_pung( $post ) { $post = get_post( $post ); if ( ! $post ) { return false; } $pung = trim( $post->pinged ); $pung = preg_split( '/\s/', $pung ); /** * Filters the list of already-pinged URLs for the given post. * * @since 2.0.0 * * @param string[] $pung Array of URLs already pinged for the given post. */ return apply_filters( 'get_pung', $pung ); } /** * Retrieves URLs that need to be pinged. * * @since 1.5.0 * @since 4.7.0 `$post` can be a WP_Post object. * * @param int|WP_Post $post Post ID or post object. * @return string[]|false List of URLs yet to ping. */ function get_to_ping( $post ) { $post = get_post( $post ); if ( ! $post ) { return false; } $to_ping = sanitize_trackback_urls( $post->to_ping ); $to_ping = preg_split( '/\s/', $to_ping, -1, PREG_SPLIT_NO_EMPTY ); /** * Filters the list of URLs yet to ping for the given post. * * @since 2.0.0 * * @param string[] $to_ping List of URLs yet to ping. */ return apply_filters( 'get_to_ping', $to_ping ); } /** * Does trackbacks for a list of URLs. * * @since 1.0.0 * * @param string $tb_list Comma separated list of URLs. * @param int $post_id Post ID. */ function trackback_url_list( $tb_list, $post_id ) { if ( ! empty( $tb_list ) ) { // Get post data. $postdata = get_post( $post_id, ARRAY_A ); // Form an excerpt. $excerpt = strip_tags( $postdata['post_excerpt'] ? $postdata['post_excerpt'] : $postdata['post_content'] ); if ( strlen( $excerpt ) > 255 ) { $excerpt = substr( $excerpt, 0, 252 ) . '…'; } $trackback_urls = explode( ',', $tb_list ); foreach ( (array) $trackback_urls as $tb_url ) { $tb_url = trim( $tb_url ); trackback( $tb_url, wp_unslash( $postdata['post_title'] ), $excerpt, $post_id ); } } } // // Page functions. // /** * Gets a list of page IDs. * * @since 2.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @return string[] List of page IDs as strings. */ function get_all_page_ids() { global $wpdb; $page_ids = wp_cache_get( 'all_page_ids', 'posts' ); if ( ! is_array( $page_ids ) ) { $page_ids = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_type = 'page'" ); wp_cache_add( 'all_page_ids', $page_ids, 'posts' ); } return $page_ids; } /** * Retrieves page data given a page ID or page object. * * Use get_post() instead of get_page(). * * @since 1.5.1 * @deprecated 3.5.0 Use get_post() * * @param int|WP_Post $page Page object or page ID. Passed by reference. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Post object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string $filter Optional. How the return value should be filtered. Accepts 'raw', * 'edit', 'db', 'display'. Default 'raw'. * @return WP_Post|array|null WP_Post or array on success, null on failure. */ function get_page( $page, $output = OBJECT, $filter = 'raw' ) { return get_post( $page, $output, $filter ); } /** * Retrieves a page given its path. * * @since 2.1.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $page_path Page path. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Post object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string|array $post_type Optional. Post type or array of post types. Default 'page'. * @return WP_Post|array|null WP_Post (or array) on success, or null on failure. */ function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) { global $wpdb; $last_changed = wp_cache_get_last_changed( 'posts' ); $hash = md5( $page_path . serialize( $post_type ) ); $cache_key = "get_page_by_path:$hash:$last_changed"; $cached = wp_cache_get( $cache_key, 'post-queries' ); if ( false !== $cached ) { // Special case: '0' is a bad `$page_path`. if ( '0' === $cached || 0 === $cached ) { return; } else { return get_post( $cached, $output ); } } $page_path = rawurlencode( urldecode( $page_path ) ); $page_path = str_replace( '%2F', '/', $page_path ); $page_path = str_replace( '%20', ' ', $page_path ); $parts = explode( '/', trim( $page_path, '/' ) ); $parts = array_map( 'sanitize_title_for_query', $parts ); $escaped_parts = esc_sql( $parts ); $in_string = "'" . implode( "','", $escaped_parts ) . "'"; if ( is_array( $post_type ) ) { $post_types = $post_type; } else { $post_types = array( $post_type, 'attachment' ); } $post_types = esc_sql( $post_types ); $post_type_in_string = "'" . implode( "','", $post_types ) . "'"; $sql = " SELECT ID, post_name, post_parent, post_type FROM $wpdb->posts WHERE post_name IN ($in_string) AND post_type IN ($post_type_in_string) "; $pages = $wpdb->get_results( $sql, OBJECT_K ); $revparts = array_reverse( $parts ); $found_id = 0; foreach ( (array) $pages as $page ) { if ( $page->post_name === $revparts[0] ) { $count = 0; $p = $page; /* * Loop through the given path parts from right to left, * ensuring each matches the post ancestry. */ while ( 0 !== (int) $p->post_parent && isset( $pages[ $p->post_parent ] ) ) { ++$count; $parent = $pages[ $p->post_parent ]; if ( ! isset( $revparts[ $count ] ) || $parent->post_name !== $revparts[ $count ] ) { break; } $p = $parent; } if ( 0 === (int) $p->post_parent && count( $revparts ) === $count + 1 && $p->post_name === $revparts[ $count ] ) { $found_id = $page->ID; if ( $page->post_type === $post_type ) { break; } } } } // We cache misses as well as hits. wp_cache_set( $cache_key, $found_id, 'post-queries' ); if ( $found_id ) { return get_post( $found_id, $output ); } return null; } /** * Identifies descendants of a given page ID in a list of page objects. * * Descendants are identified from the `$pages` array passed to the function. No database queries are performed. * * @since 1.5.1 * * @param int $page_id Page ID. * @param WP_Post[] $pages List of page objects from which descendants should be identified. * @return WP_Post[] List of page children. */ function get_page_children( $page_id, $pages ) { // Build a hash of ID -> children. $children = array(); foreach ( (array) $pages as $page ) { $children[ (int) $page->post_parent ][] = $page; } $page_list = array(); // Start the search by looking at immediate children. if ( isset( $children[ $page_id ] ) ) { // Always start at the end of the stack in order to preserve original `$pages` order. $to_look = array_reverse( $children[ $page_id ] ); while ( $to_look ) { $p = array_pop( $to_look ); $page_list[] = $p; if ( isset( $children[ $p->ID ] ) ) { foreach ( array_reverse( $children[ $p->ID ] ) as $child ) { // Append to the `$to_look` stack to descend the tree. $to_look[] = $child; } } } } return $page_list; } /** * Orders the pages with children under parents in a flat list. * * It uses auxiliary structure to hold parent-children relationships and * runs in O(N) complexity * * @since 2.0.0 * * @param WP_Post[] $pages Posts array (passed by reference). * @param int $page_id Optional. Parent page ID. Default 0. * @return string[] Array of post names keyed by ID and arranged by hierarchy. Children immediately follow their parents. */ function get_page_hierarchy( &$pages, $page_id = 0 ) { if ( empty( $pages ) ) { return array(); } $children = array(); foreach ( (array) $pages as $p ) { $parent_id = (int) $p->post_parent; $children[ $parent_id ][] = $p; } $result = array(); _page_traverse_name( $page_id, $children, $result ); return $result; } /** * Traverses and return all the nested children post names of a root page. * * $children contains parent-children relations * * @since 2.9.0 * @access private * * @see _page_traverse_name() * * @param int $page_id Page ID. * @param array $children Parent-children relations (passed by reference). * @param string[] $result Array of page names keyed by ID (passed by reference). */ function _page_traverse_name( $page_id, &$children, &$result ) { if ( isset( $children[ $page_id ] ) ) { foreach ( (array) $children[ $page_id ] as $child ) { $result[ $child->ID ] = $child->post_name; _page_traverse_name( $child->ID, $children, $result ); } } } /** * Builds the URI path for a page. * * Sub pages will be in the "directory" under the parent page post name. * * @since 1.5.0 * @since 4.6.0 The `$page` parameter was made optional. * * @param WP_Post|object|int $page Optional. Page ID or WP_Post object. Default is global $post. * @return string|false Page URI, false on error. */ function get_page_uri( $page = 0 ) { if ( ! $page instanceof WP_Post ) { $page = get_post( $page ); } if ( ! $page ) { return false; } $uri = $page->post_name; foreach ( $page->ancestors as $parent ) { $parent = get_post( $parent ); if ( $parent && $parent->post_name ) { $uri = $parent->post_name . '/' . $uri; } } /** * Filters the URI for a page. * * @since 4.4.0 * * @param string $uri Page URI. * @param WP_Post $page Page object. */ return apply_filters( 'get_page_uri', $uri, $page ); } /** * Retrieves an array of pages (or hierarchical post type items). * * @since 1.5.0 * @since 6.3.0 Use WP_Query internally. * * @param array|string $args { * Optional. Array or string of arguments to retrieve pages. * * @type int $child_of Page ID to return child and grandchild pages of. Note: The value * of `$hierarchical` has no bearing on whether `$child_of` returns * hierarchical results. Default 0, or no restriction. * @type string $sort_order How to sort retrieved pages. Accepts 'ASC', 'DESC'. Default 'ASC'. * @type string $sort_column What columns to sort pages by, comma-separated. Accepts 'post_author', * 'post_date', 'post_title', 'post_name', 'post_modified', 'menu_order', * 'post_modified_gmt', 'post_parent', 'ID', 'rand', 'comment_count'. * 'post_' can be omitted for any values that start with it. * Default 'post_title'. * @type bool $hierarchical Whether to return pages hierarchically. If false in conjunction with * `$child_of` also being false, both arguments will be disregarded. * Default true. * @type int[] $exclude Array of page IDs to exclude. Default empty array. * @type int[] $include Array of page IDs to include. Cannot be used with `$child_of`, * `$parent`, `$exclude`, `$meta_key`, `$meta_value`, or `$hierarchical`. * Default empty array. * @type string $meta_key Only include pages with this meta key. Default empty. * @type string $meta_value Only include pages with this meta value. Requires `$meta_key`. * Default empty. * @type string $authors A comma-separated list of author IDs. Default empty. * @type int $parent Page ID to return direct children of. Default -1, or no restriction. * @type string|int[] $exclude_tree Comma-separated string or array of page IDs to exclude. * Default empty array. * @type int $number The number of pages to return. Default 0, or all pages. * @type int $offset The number of pages to skip before returning. Requires `$number`. * Default 0. * @type string $post_type The post type to query. Default 'page'. * @type string|array $post_status A comma-separated list or array of post statuses to include. * Default 'publish'. * } * @return WP_Post[]|false Array of pages (or hierarchical post type items). Boolean false if the * specified post type is not hierarchical or the specified status is not * supported by the post type. */ function get_pages( $args = array() ) { $defaults = array( 'child_of' => 0, 'sort_order' => 'ASC', 'sort_column' => 'post_title', 'hierarchical' => 1, 'exclude' => array(), 'include' => array(), 'meta_key' => '', 'meta_value' => '', 'authors' => '', 'parent' => -1, 'exclude_tree' => array(), 'number' => '', 'offset' => 0, 'post_type' => 'page', 'post_status' => 'publish', ); $parsed_args = wp_parse_args( $args, $defaults ); $number = (int) $parsed_args['number']; $offset = (int) $parsed_args['offset']; $child_of = (int) $parsed_args['child_of']; $hierarchical = $parsed_args['hierarchical']; $exclude = $parsed_args['exclude']; $meta_key = $parsed_args['meta_key']; $meta_value = $parsed_args['meta_value']; $parent = $parsed_args['parent']; $post_status = $parsed_args['post_status']; // Make sure the post type is hierarchical. $hierarchical_post_types = get_post_types( array( 'hierarchical' => true ) ); if ( ! in_array( $parsed_args['post_type'], $hierarchical_post_types, true ) ) { return false; } if ( $parent > 0 && ! $child_of ) { $hierarchical = false; } // Make sure we have a valid post status. if ( ! is_array( $post_status ) ) { $post_status = explode( ',', $post_status ); } if ( array_diff( $post_status, get_post_stati() ) ) { return false; } $query_args = array( 'orderby' => 'post_title', 'order' => 'ASC', 'post__not_in' => wp_parse_id_list( $exclude ), 'meta_key' => $meta_key, 'meta_value' => $meta_value, 'posts_per_page' => -1, 'offset' => $offset, 'post_type' => $parsed_args['post_type'], 'post_status' => $post_status, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, 'ignore_sticky_posts' => true, 'no_found_rows' => true, ); if ( ! empty( $parsed_args['include'] ) ) { $child_of = 0; // Ignore child_of, parent, exclude, meta_key, and meta_value params if using include. $parent = -1; unset( $query_args['post__not_in'], $query_args['meta_key'], $query_args['meta_value'] ); $hierarchical = false; $query_args['post__in'] = wp_parse_id_list( $parsed_args['include'] ); } if ( ! empty( $parsed_args['authors'] ) ) { $post_authors = wp_parse_list( $parsed_args['authors'] ); if ( ! empty( $post_authors ) ) { $query_args['author__in'] = array(); foreach ( $post_authors as $post_author ) { // Do we have an author id or an author login? if ( 0 === (int) $post_author ) { $post_author = get_user_by( 'login', $post_author ); if ( empty( $post_author ) ) { continue; } if ( empty( $post_author->ID ) ) { continue; } $post_author = $post_author->ID; } $query_args['author__in'][] = (int) $post_author; } } } if ( is_array( $parent ) ) { $post_parent__in = array_map( 'absint', (array) $parent ); if ( ! empty( $post_parent__in ) ) { $query_args['post_parent__in'] = $post_parent__in; } } elseif ( $parent >= 0 ) { $query_args['post_parent'] = $parent; } /* * Maintain backward compatibility for `sort_column` key. * Additionally to `WP_Query`, it has been supporting the `post_modified_gmt` field, so this logic will translate * it to `post_modified` which should result in the same order given the two dates in the fields match. */ $orderby = wp_parse_list( $parsed_args['sort_column'] ); $orderby = array_map( static function ( $orderby_field ) { $orderby_field = trim( $orderby_field ); if ( 'post_modified_gmt' === $orderby_field || 'modified_gmt' === $orderby_field ) { $orderby_field = str_replace( '_gmt', '', $orderby_field ); } return $orderby_field; }, $orderby ); if ( $orderby ) { $query_args['orderby'] = array_fill_keys( $orderby, $parsed_args['sort_order'] ); } $order = $parsed_args['sort_order']; if ( $order ) { $query_args['order'] = $order; } if ( ! empty( $number ) ) { $query_args['posts_per_page'] = $number; } /** * Filters query arguments passed to WP_Query in get_pages. * * @since 6.3.0 * * @param array $query_args Array of arguments passed to WP_Query. * @param array $parsed_args Array of get_pages() arguments. */ $query_args = apply_filters( 'get_pages_query_args', $query_args, $parsed_args ); $pages = new WP_Query(); $pages = $pages->query( $query_args ); if ( $child_of || $hierarchical ) { $pages = get_page_children( $child_of, $pages ); } if ( ! empty( $parsed_args['exclude_tree'] ) ) { $exclude = wp_parse_id_list( $parsed_args['exclude_tree'] ); foreach ( $exclude as $id ) { $children = get_page_children( $id, $pages ); foreach ( $children as $child ) { $exclude[] = $child->ID; } } $num_pages = count( $pages ); for ( $i = 0; $i < $num_pages; $i++ ) { if ( in_array( $pages[ $i ]->ID, $exclude, true ) ) { unset( $pages[ $i ] ); } } } /** * Filters the retrieved list of pages. * * @since 2.1.0 * * @param WP_Post[] $pages Array of page objects. * @param array $parsed_args Array of get_pages() arguments. */ return apply_filters( 'get_pages', $pages, $parsed_args ); } // // Attachment functions. // /** * Determines whether an attachment URI is local and really an attachment. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 2.0.0 * * @param string $url URL to check * @return bool True on success, false on failure. */ function is_local_attachment( $url ) { if ( ! str_contains( $url, home_url() ) ) { return false; } if ( str_contains( $url, home_url( '/?attachment_id=' ) ) ) { return true; } $id = url_to_postid( $url ); if ( $id ) { $post = get_post( $id ); if ( 'attachment' === $post->post_type ) { return true; } } return false; } /** * Inserts an attachment. * * If you set the 'ID' in the $args parameter, it will mean that you are * updating and attempt to update the attachment. You can also set the * attachment name or title by setting the key 'post_name' or 'post_title'. * * You can set the dates for the attachment manually by setting the 'post_date' * and 'post_date_gmt' keys' values. * * By default, the comments will use the default settings for whether the * comments are allowed. You can close them manually or keep them open by * setting the value for the 'comment_status' key. * * @since 2.0.0 * @since 4.7.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure. * @since 5.6.0 Added the `$fire_after_hooks` parameter. * * @see wp_insert_post() * * @param string|array $args Arguments for inserting an attachment. * @param string|false $file Optional. Filename. Default false. * @param int $parent_post_id Optional. Parent post ID or 0 for no parent. Default 0. * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true. * @return int|WP_Error The attachment ID on success. The value 0 or WP_Error on failure. */ function wp_insert_attachment( $args, $file = false, $parent_post_id = 0, $wp_error = false, $fire_after_hooks = true ) { $defaults = array( 'file' => $file, 'post_parent' => 0, ); $data = wp_parse_args( $args, $defaults ); if ( ! empty( $parent_post_id ) ) { $data['post_parent'] = $parent_post_id; } $data['post_type'] = 'attachment'; return wp_insert_post( $data, $wp_error, $fire_after_hooks ); } /** * Trashes or deletes an attachment. * * When an attachment is permanently deleted, the file will also be removed. * Deletion removes all post meta fields, taxonomy, comments, etc. associated * with the attachment (except the main post). * * The attachment is moved to the Trash instead of permanently deleted unless Trash * for media is disabled, item is already in the Trash, or $force_delete is true. * * @since 2.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $post_id Attachment ID. * @param bool $force_delete Optional. Whether to bypass Trash and force deletion. * Default false. * @return WP_Post|false|null Post data on success, false or null on failure. */ function wp_delete_attachment( $post_id, $force_delete = false ) { global $wpdb; $post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id ) ); if ( ! $post ) { return $post; } $post = get_post( $post ); if ( 'attachment' !== $post->post_type ) { return false; } if ( ! $force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' !== $post->post_status ) { return wp_trash_post( $post_id ); } /** * Filters whether an attachment deletion should take place. * * @since 5.5.0 * * @param WP_Post|false|null $delete Whether to go forward with deletion. * @param WP_Post $post Post object. * @param bool $force_delete Whether to bypass the Trash. */ $check = apply_filters( 'pre_delete_attachment', null, $post, $force_delete ); if ( null !== $check ) { return $check; } delete_post_meta( $post_id, '_wp_trash_meta_status' ); delete_post_meta( $post_id, '_wp_trash_meta_time' ); $meta = wp_get_attachment_metadata( $post_id ); $backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true ); $file = get_attached_file( $post_id ); if ( is_multisite() && is_string( $file ) && ! empty( $file ) ) { clean_dirsize_cache( $file ); } /** * Fires before an attachment is deleted, at the start of wp_delete_attachment(). * * @since 2.0.0 * @since 5.5.0 Added the `$post` parameter. * * @param int $post_id Attachment ID. * @param WP_Post $post Post object. */ do_action( 'delete_attachment', $post_id, $post ); wp_delete_object_term_relationships( $post_id, array( 'category', 'post_tag' ) ); wp_delete_object_term_relationships( $post_id, get_object_taxonomies( $post->post_type ) ); // Delete all for any posts. delete_metadata( 'post', null, '_thumbnail_id', $post_id, true ); wp_defer_comment_counting( true ); $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d ORDER BY comment_ID DESC", $post_id ) ); foreach ( $comment_ids as $comment_id ) { wp_delete_comment( $comment_id, true ); } wp_defer_comment_counting( false ); $post_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d ", $post_id ) ); foreach ( $post_meta_ids as $mid ) { delete_metadata_by_mid( 'post', $mid ); } /** This action is documented in wp-includes/post.php */ do_action( 'delete_post', $post_id, $post ); $result = $wpdb->delete( $wpdb->posts, array( 'ID' => $post_id ) ); if ( ! $result ) { return false; } /** This action is documented in wp-includes/post.php */ do_action( 'deleted_post', $post_id, $post ); wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file ); clean_post_cache( $post ); return $post; } /** * Deletes all files that belong to the given attachment. * * @since 4.9.7 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $post_id Attachment ID. * @param array $meta The attachment's meta data. * @param array $backup_sizes The meta data for the attachment's backup images. * @param string $file Absolute path to the attachment's file. * @return bool True on success, false on failure. */ function wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file ) { global $wpdb; $uploadpath = wp_get_upload_dir(); $deleted = true; if ( ! empty( $meta['thumb'] ) ) { // Don't delete the thumb if another attachment uses it. if ( ! $wpdb->get_row( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attachment_metadata' AND meta_value LIKE %s AND post_id <> %d", '%' . $wpdb->esc_like( $meta['thumb'] ) . '%', $post_id ) ) ) { $thumbfile = str_replace( wp_basename( $file ), $meta['thumb'], $file ); if ( ! empty( $thumbfile ) ) { $thumbfile = path_join( $uploadpath['basedir'], $thumbfile ); $thumbdir = path_join( $uploadpath['basedir'], dirname( $file ) ); if ( ! wp_delete_file_from_directory( $thumbfile, $thumbdir ) ) { $deleted = false; } } } } // Remove intermediate and backup images if there are any. if ( isset( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) { $intermediate_dir = path_join( $uploadpath['basedir'], dirname( $file ) ); foreach ( $meta['sizes'] as $size => $sizeinfo ) { $intermediate_file = str_replace( wp_basename( $file ), $sizeinfo['file'], $file ); if ( ! empty( $intermediate_file ) ) { $intermediate_file = path_join( $uploadpath['basedir'], $intermediate_file ); if ( ! wp_delete_file_from_directory( $intermediate_file, $intermediate_dir ) ) { $deleted = false; } } } } if ( ! empty( $meta['original_image'] ) ) { if ( empty( $intermediate_dir ) ) { $intermediate_dir = path_join( $uploadpath['basedir'], dirname( $file ) ); } $original_image = str_replace( wp_basename( $file ), $meta['original_image'], $file ); if ( ! empty( $original_image ) ) { $original_image = path_join( $uploadpath['basedir'], $original_image ); if ( ! wp_delete_file_from_directory( $original_image, $intermediate_dir ) ) { $deleted = false; } } } if ( is_array( $backup_sizes ) ) { $del_dir = path_join( $uploadpath['basedir'], dirname( $meta['file'] ) ); foreach ( $backup_sizes as $size ) { $del_file = path_join( dirname( $meta['file'] ), $size['file'] ); if ( ! empty( $del_file ) ) { $del_file = path_join( $uploadpath['basedir'], $del_file ); if ( ! wp_delete_file_from_directory( $del_file, $del_dir ) ) { $deleted = false; } } } } if ( ! wp_delete_file_from_directory( $file, $uploadpath['basedir'] ) ) { $deleted = false; } return $deleted; } /** * Retrieves attachment metadata for attachment ID. * * @since 2.1.0 * @since 6.0.0 The `$filesize` value was added to the returned array. * * @param int $attachment_id Attachment post ID. Defaults to global $post. * @param bool $unfiltered Optional. If true, filters are not run. Default false. * @return array|false { * Attachment metadata. False on failure. * * @type int $width The width of the attachment. * @type int $height The height of the attachment. * @type string $file The file path relative to `wp-content/uploads`. * @type array $sizes Keys are size slugs, each value is an array containing * 'file', 'width', 'height', and 'mime-type'. * @type array $image_meta Image metadata. * @type int $filesize File size of the attachment. * } */ function wp_get_attachment_metadata( $attachment_id = 0, $unfiltered = false ) { $attachment_id = (int) $attachment_id; if ( ! $attachment_id ) { $post = get_post(); if ( ! $post ) { return false; } $attachment_id = $post->ID; } $data = get_post_meta( $attachment_id, '_wp_attachment_metadata', true ); if ( ! $data ) { return false; } if ( $unfiltered ) { return $data; } /** * Filters the attachment meta data. * * @since 2.1.0 * * @param array $data Array of meta data for the given attachment. * @param int $attachment_id Attachment post ID. */ return apply_filters( 'wp_get_attachment_metadata', $data, $attachment_id ); } /** * Updates metadata for an attachment. * * @since 2.1.0 * * @param int $attachment_id Attachment post ID. * @param array $data Attachment meta data. * @return int|false False if $post is invalid. */ function wp_update_attachment_metadata( $attachment_id, $data ) { $attachment_id = (int) $attachment_id; $post = get_post( $attachment_id ); if ( ! $post ) { return false; } /** * Filters the updated attachment meta data. * * @since 2.1.0 * * @param array $data Array of updated attachment meta data. * @param int $attachment_id Attachment post ID. */ $data = apply_filters( 'wp_update_attachment_metadata', $data, $post->ID ); if ( $data ) { return update_post_meta( $post->ID, '_wp_attachment_metadata', $data ); } else { return delete_post_meta( $post->ID, '_wp_attachment_metadata' ); } } /** * Retrieves the URL for an attachment. * * @since 2.1.0 * * @global string $pagenow The filename of the current screen. * * @param int $attachment_id Optional. Attachment post ID. Defaults to global $post. * @return string|false Attachment URL, otherwise false. */ function wp_get_attachment_url( $attachment_id = 0 ) { global $pagenow; $attachment_id = (int) $attachment_id; $post = get_post( $attachment_id ); if ( ! $post ) { return false; } if ( 'attachment' !== $post->post_type ) { return false; } $url = ''; // Get attached file. $file = get_post_meta( $post->ID, '_wp_attached_file', true ); if ( $file ) { // Get upload directory. $uploads = wp_get_upload_dir(); if ( $uploads && false === $uploads['error'] ) { // Check that the upload base exists in the file location. if ( str_starts_with( $file, $uploads['basedir'] ) ) { // Replace file location with url location. $url = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); } elseif ( str_contains( $file, 'wp-content/uploads' ) ) { // Get the directory name relative to the basedir (back compat for pre-2.7 uploads). $url = trailingslashit( $uploads['baseurl'] . '/' . _wp_get_attachment_relative_path( $file ) ) . wp_basename( $file ); } else { // It's a newly-uploaded file, therefore $file is relative to the basedir. $url = $uploads['baseurl'] . "/$file"; } } } /* * If any of the above options failed, Fallback on the GUID as used pre-2.7, * not recommended to rely upon this. */ if ( ! $url ) { $url = get_the_guid( $post->ID ); } // On SSL front end, URLs should be HTTPS. if ( is_ssl() && ! is_admin() && 'wp-login.php' !== $pagenow ) { $url = set_url_scheme( $url ); } /** * Filters the attachment URL. * * @since 2.1.0 * * @param string $url URL for the given attachment. * @param int $attachment_id Attachment post ID. */ $url = apply_filters( 'wp_get_attachment_url', $url, $post->ID ); if ( ! $url ) { return false; } return $url; } /** * Retrieves the caption for an attachment. * * @since 4.6.0 * * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`. * @return string|false Attachment caption on success, false on failure. */ function wp_get_attachment_caption( $post_id = 0 ) { $post_id = (int) $post_id; $post = get_post( $post_id ); if ( ! $post ) { return false; } if ( 'attachment' !== $post->post_type ) { return false; } $caption = $post->post_excerpt; /** * Filters the attachment caption. * * @since 4.6.0 * * @param string $caption Caption for the given attachment. * @param int $post_id Attachment ID. */ return apply_filters( 'wp_get_attachment_caption', $caption, $post->ID ); } /** * Retrieves URL for an attachment thumbnail. * * @since 2.1.0 * @since 6.1.0 Changed to use wp_get_attachment_image_url(). * * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`. * @return string|false Thumbnail URL on success, false on failure. */ function wp_get_attachment_thumb_url( $post_id = 0 ) { $post_id = (int) $post_id; /* * This uses image_downsize() which also looks for the (very) old format $image_meta['thumb'] * when the newer format $image_meta['sizes']['thumbnail'] doesn't exist. */ $thumbnail_url = wp_get_attachment_image_url( $post_id, 'thumbnail' ); if ( empty( $thumbnail_url ) ) { return false; } /** * Filters the attachment thumbnail URL. * * @since 2.1.0 * * @param string $thumbnail_url URL for the attachment thumbnail. * @param int $post_id Attachment ID. */ return apply_filters( 'wp_get_attachment_thumb_url', $thumbnail_url, $post_id ); } /** * Verifies an attachment is of a given type. * * @since 4.2.0 * * @param string $type Attachment type. Accepts `image`, `audio`, `video`, or a file extension. * @param int|WP_Post $post Optional. Attachment ID or object. Default is global $post. * @return bool True if an accepted type or a matching file extension, false otherwise. */ function wp_attachment_is( $type, $post = null ) { $post = get_post( $post ); if ( ! $post ) { return false; } $file = get_attached_file( $post->ID ); if ( ! $file ) { return false; } if ( str_starts_with( $post->post_mime_type, $type . '/' ) ) { return true; } $check = wp_check_filetype( $file ); if ( empty( $check['ext'] ) ) { return false; } $ext = $check['ext']; if ( 'import' !== $post->post_mime_type ) { return $type === $ext; } switch ( $type ) { case 'image': $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif', 'heic' ); return in_array( $ext, $image_exts, true ); case 'audio': return in_array( $ext, wp_get_audio_extensions(), true ); case 'video': return in_array( $ext, wp_get_video_extensions(), true ); default: return $type === $ext; } } /** * Determines whether an attachment is an image. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 2.1.0 * @since 4.2.0 Modified into wrapper for wp_attachment_is() and * allowed WP_Post object to be passed. * * @param int|WP_Post $post Optional. Attachment ID or object. Default is global $post. * @return bool Whether the attachment is an image. */ function wp_attachment_is_image( $post = null ) { return wp_attachment_is( 'image', $post ); } /** * Retrieves the icon for a MIME type or attachment. * * @since 2.1.0 * @since 6.5.0 Added the `$preferred_ext` parameter. * * @param string|int $mime MIME type or attachment ID. * @param string $preferred_ext File format to prefer in return. Default '.png'. * @return string|false Icon, false otherwise. */ function wp_mime_type_icon( $mime = 0, $preferred_ext = '.png' ) { if ( ! is_numeric( $mime ) ) { $icon = wp_cache_get( "mime_type_icon_$mime" ); } // Check if preferred file format variable is present and is a validly formatted file extension. if ( ! empty( $preferred_ext ) && is_string( $preferred_ext ) && ! str_starts_with( $preferred_ext, '.' ) ) { $preferred_ext = '.' . strtolower( $preferred_ext ); } $post_id = 0; if ( empty( $icon ) ) { $post_mimes = array(); if ( is_numeric( $mime ) ) { $mime = (int) $mime; $post = get_post( $mime ); if ( $post ) { $post_id = (int) $post->ID; $file = get_attached_file( $post_id ); $ext = preg_replace( '/^.+?\.([^.]+)$/', '$1', $file ); if ( ! empty( $ext ) ) { $post_mimes[] = $ext; $ext_type = wp_ext2type( $ext ); if ( $ext_type ) { $post_mimes[] = $ext_type; } } $mime = $post->post_mime_type; } else { $mime = 0; } } else { $post_mimes[] = $mime; } $icon_files = wp_cache_get( 'icon_files' ); if ( ! is_array( $icon_files ) ) { /** * Filters the icon directory path. * * @since 2.0.0 * * @param string $path Icon directory absolute path. */ $icon_dir = apply_filters( 'icon_dir', ABSPATH . WPINC . '/images/media' ); /** * Filters the icon directory URI. * * @since 2.0.0 * * @param string $uri Icon directory URI. */ $icon_dir_uri = apply_filters( 'icon_dir_uri', includes_url( 'images/media' ) ); /** * Filters the array of icon directory URIs. * * @since 2.5.0 * * @param string[] $uris Array of icon directory URIs keyed by directory absolute path. */ $dirs = apply_filters( 'icon_dirs', array( $icon_dir => $icon_dir_uri ) ); $icon_files = array(); $all_icons = array(); while ( $dirs ) { $keys = array_keys( $dirs ); $dir = array_shift( $keys ); $uri = array_shift( $dirs ); $dh = opendir( $dir ); if ( $dh ) { while ( false !== $file = readdir( $dh ) ) { $file = wp_basename( $file ); if ( str_starts_with( $file, '.' ) ) { continue; } $ext = strtolower( substr( $file, -4 ) ); if ( ! in_array( $ext, array( '.svg', '.png', '.gif', '.jpg' ), true ) ) { if ( is_dir( "$dir/$file" ) ) { $dirs[ "$dir/$file" ] = "$uri/$file"; } continue; } $all_icons[ "$dir/$file" ] = "$uri/$file"; if ( $ext === $preferred_ext ) { $icon_files[ "$dir/$file" ] = "$uri/$file"; } } closedir( $dh ); } } // If directory only contained icons of a non-preferred format, return those. if ( empty( $icon_files ) ) { $icon_files = $all_icons; } wp_cache_add( 'icon_files', $icon_files, 'default', 600 ); } $types = array(); // Icon wp_basename - extension = MIME wildcard. foreach ( $icon_files as $file => $uri ) { $types[ preg_replace( '/^([^.]*).*$/', '$1', wp_basename( $file ) ) ] =& $icon_files[ $file ]; } if ( ! empty( $mime ) ) { $post_mimes[] = substr( $mime, 0, strpos( $mime, '/' ) ); $post_mimes[] = substr( $mime, strpos( $mime, '/' ) + 1 ); $post_mimes[] = str_replace( '/', '_', $mime ); } $matches = wp_match_mime_types( array_keys( $types ), $post_mimes ); $matches['default'] = array( 'default' ); foreach ( $matches as $match => $wilds ) { foreach ( $wilds as $wild ) { if ( ! isset( $types[ $wild ] ) ) { continue; } $icon = $types[ $wild ]; if ( ! is_numeric( $mime ) ) { wp_cache_add( "mime_type_icon_$mime", $icon ); } break 2; } } } /** * Filters the mime type icon. * * @since 2.1.0 * * @param string $icon Path to the mime type icon. * @param string $mime Mime type. * @param int $post_id Attachment ID. Will equal 0 if the function passed * the mime type. */ return apply_filters( 'wp_mime_type_icon', $icon, $mime, $post_id ); } /** * Checks for changed slugs for published post objects and save the old slug. * * The function is used when a post object of any type is updated, * by comparing the current and previous post objects. * * If the slug was changed and not already part of the old slugs then it will be * added to the post meta field ('_wp_old_slug') for storing old slugs for that * post. * * The most logically usage of this function is redirecting changed post objects, so * that those that linked to an changed post will be redirected to the new post. * * @since 2.1.0 * * @param int $post_id Post ID. * @param WP_Post $post The post object. * @param WP_Post $post_before The previous post object. */ function wp_check_for_changed_slugs( $post_id, $post, $post_before ) { // Don't bother if it hasn't changed. if ( $post->post_name === $post_before->post_name ) { return; } // We're only concerned with published, non-hierarchical objects. if ( ! ( 'publish' === $post->post_status || ( 'attachment' === $post->post_type && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) { return; } $old_slugs = (array) get_post_meta( $post_id, '_wp_old_slug' ); // If we haven't added this old slug before, add it now. if ( ! empty( $post_before->post_name ) && ! in_array( $post_before->post_name, $old_slugs, true ) ) { add_post_meta( $post_id, '_wp_old_slug', $post_before->post_name ); } // If the new slug was used previously, delete it from the list. if ( in_array( $post->post_name, $old_slugs, true ) ) { delete_post_meta( $post_id, '_wp_old_slug', $post->post_name ); } } /** * Checks for changed dates for published post objects and save the old date. * * The function is used when a post object of any type is updated, * by comparing the current and previous post objects. * * If the date was changed and not already part of the old dates then it will be * added to the post meta field ('_wp_old_date') for storing old dates for that * post. * * The most logically usage of this function is redirecting changed post objects, so * that those that linked to an changed post will be redirected to the new post. * * @since 4.9.3 * * @param int $post_id Post ID. * @param WP_Post $post The post object. * @param WP_Post $post_before The previous post object. */ function wp_check_for_changed_dates( $post_id, $post, $post_before ) { $previous_date = gmdate( 'Y-m-d', strtotime( $post_before->post_date ) ); $new_date = gmdate( 'Y-m-d', strtotime( $post->post_date ) ); // Don't bother if it hasn't changed. if ( $new_date === $previous_date ) { return; } // We're only concerned with published, non-hierarchical objects. if ( ! ( 'publish' === $post->post_status || ( 'attachment' === $post->post_type && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) { return; } $old_dates = (array) get_post_meta( $post_id, '_wp_old_date' ); // If we haven't added this old date before, add it now. if ( ! empty( $previous_date ) && ! in_array( $previous_date, $old_dates, true ) ) { add_post_meta( $post_id, '_wp_old_date', $previous_date ); } // If the new slug was used previously, delete it from the list. if ( in_array( $new_date, $old_dates, true ) ) { delete_post_meta( $post_id, '_wp_old_date', $new_date ); } } /** * Retrieves the private post SQL based on capability. * * This function provides a standardized way to appropriately select on the * post_status of a post type. The function will return a piece of SQL code * that can be added to a WHERE clause; this SQL is constructed to allow all * published posts, and all private posts to which the user has access. * * @since 2.2.0 * @since 4.3.0 Added the ability to pass an array to `$post_type`. * * @param string|array $post_type Single post type or an array of post types. Currently only supports 'post' or 'page'. * @return string SQL code that can be added to a where clause. */ function get_private_posts_cap_sql( $post_type ) { return get_posts_by_author_sql( $post_type, false ); } /** * Retrieves the post SQL based on capability, author, and type. * * @since 3.0.0 * @since 4.3.0 Introduced the ability to pass an array of post types to `$post_type`. * * @see get_private_posts_cap_sql() * @global wpdb $wpdb WordPress database abstraction object. * * @param string|string[] $post_type Single post type or an array of post types. * @param bool $full Optional. Returns a full WHERE statement instead of just * an 'andalso' term. Default true. * @param int $post_author Optional. Query posts having a single author ID. Default null. * @param bool $public_only Optional. Only return public posts. Skips cap checks for * $current_user. Default false. * @return string SQL WHERE code that can be added to a query. */ function get_posts_by_author_sql( $post_type, $full = true, $post_author = null, $public_only = false ) { global $wpdb; if ( is_array( $post_type ) ) { $post_types = $post_type; } else { $post_types = array( $post_type ); } $post_type_clauses = array(); foreach ( $post_types as $post_type ) { $post_type_obj = get_post_type_object( $post_type ); if ( ! $post_type_obj ) { continue; } /** * Filters the capability to read private posts for a custom post type * when generating SQL for getting posts by author. * * @since 2.2.0 * @deprecated 3.2.0 The hook transitioned from "somewhat useless" to "totally useless". * * @param string $cap Capability. */ $cap = apply_filters_deprecated( 'pub_priv_sql_capability', array( '' ), '3.2.0' ); if ( ! $cap ) { $cap = current_user_can( $post_type_obj->cap->read_private_posts ); } // Only need to check the cap if $public_only is false. $post_status_sql = "post_status = 'publish'"; if ( false === $public_only ) { if ( $cap ) { // Does the user have the capability to view private posts? Guess so. $post_status_sql .= " OR post_status = 'private'"; } elseif ( is_user_logged_in() ) { // Users can view their own private posts. $id = get_current_user_id(); if ( null === $post_author || ! $full ) { $post_status_sql .= " OR post_status = 'private' AND post_author = $id"; } elseif ( $id === (int) $post_author ) { $post_status_sql .= " OR post_status = 'private'"; } // Else none. } // Else none. } $post_type_clauses[] = "( post_type = '" . $post_type . "' AND ( $post_status_sql ) )"; } if ( empty( $post_type_clauses ) ) { return $full ? 'WHERE 1 = 0' : '1 = 0'; } $sql = '( ' . implode( ' OR ', $post_type_clauses ) . ' )'; if ( null !== $post_author ) { $sql .= $wpdb->prepare( ' AND post_author = %d', $post_author ); } if ( $full ) { $sql = 'WHERE ' . $sql; } return $sql; } /** * Retrieves the most recent time that a post on the site was published. * * The server timezone is the default and is the difference between GMT and * server time. The 'blog' value is the date when the last post was posted. * The 'gmt' is when the last post was posted in GMT formatted date. * * @since 0.71 * @since 4.4.0 The `$post_type` argument was added. * * @param string $timezone Optional. The timezone for the timestamp. Accepts 'server', 'blog', or 'gmt'. * 'server' uses the server's internal timezone. * 'blog' uses the `post_date` field, which proxies to the timezone set for the site. * 'gmt' uses the `post_date_gmt` field. * Default 'server'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string The date of the last post, or false on failure. */ function get_lastpostdate( $timezone = 'server', $post_type = 'any' ) { $lastpostdate = _get_last_post_time( $timezone, 'date', $post_type ); /** * Filters the most recent time that a post on the site was published. * * @since 2.3.0 * @since 5.5.0 Added the `$post_type` parameter. * * @param string|false $lastpostdate The most recent time that a post was published, * in 'Y-m-d H:i:s' format. False on failure. * @param string $timezone Location to use for getting the post published date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ return apply_filters( 'get_lastpostdate', $lastpostdate, $timezone, $post_type ); } /** * Gets the most recent time that a post on the site was modified. * * The server timezone is the default and is the difference between GMT and * server time. The 'blog' value is just when the last post was modified. * The 'gmt' is when the last post was modified in GMT time. * * @since 1.2.0 * @since 4.4.0 The `$post_type` argument was added. * * @param string $timezone Optional. The timezone for the timestamp. See get_lastpostdate() * for information on accepted values. * Default 'server'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string The timestamp in 'Y-m-d H:i:s' format, or false on failure. */ function get_lastpostmodified( $timezone = 'server', $post_type = 'any' ) { /** * Pre-filter the return value of get_lastpostmodified() before the query is run. * * @since 4.4.0 * * @param string|false $lastpostmodified The most recent time that a post was modified, * in 'Y-m-d H:i:s' format, or false. Returning anything * other than false will short-circuit the function. * @param string $timezone Location to use for getting the post modified date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ $lastpostmodified = apply_filters( 'pre_get_lastpostmodified', false, $timezone, $post_type ); if ( false !== $lastpostmodified ) { return $lastpostmodified; } $lastpostmodified = _get_last_post_time( $timezone, 'modified', $post_type ); $lastpostdate = get_lastpostdate( $timezone, $post_type ); if ( $lastpostdate > $lastpostmodified ) { $lastpostmodified = $lastpostdate; } /** * Filters the most recent time that a post on the site was modified. * * @since 2.3.0 * @since 5.5.0 Added the `$post_type` parameter. * * @param string|false $lastpostmodified The most recent time that a post was modified, * in 'Y-m-d H:i:s' format. False on failure. * @param string $timezone Location to use for getting the post modified date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ return apply_filters( 'get_lastpostmodified', $lastpostmodified, $timezone, $post_type ); } /** * Gets the timestamp of the last time any post was modified or published. * * @since 3.1.0 * @since 4.4.0 The `$post_type` argument was added. * @access private * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $timezone The timezone for the timestamp. See get_lastpostdate(). * for information on accepted values. * @param string $field Post field to check. Accepts 'date' or 'modified'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string|false The timestamp in 'Y-m-d H:i:s' format, or false on failure. */ function _get_last_post_time( $timezone, $field, $post_type = 'any' ) { global $wpdb; if ( ! in_array( $field, array( 'date', 'modified' ), true ) ) { return false; } $timezone = strtolower( $timezone ); $key = "lastpost{$field}:$timezone"; if ( 'any' !== $post_type ) { $key .= ':' . sanitize_key( $post_type ); } $date = wp_cache_get( $key, 'timeinfo' ); if ( false !== $date ) { return $date; } if ( 'any' === $post_type ) { $post_types = get_post_types( array( 'public' => true ) ); array_walk( $post_types, array( $wpdb, 'escape_by_ref' ) ); $post_types = "'" . implode( "', '", $post_types ) . "'"; } else { $post_types = "'" . sanitize_key( $post_type ) . "'"; } switch ( $timezone ) { case 'gmt': $date = $wpdb->get_var( "SELECT post_{$field}_gmt FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; case 'blog': $date = $wpdb->get_var( "SELECT post_{$field} FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; case 'server': $add_seconds_server = gmdate( 'Z' ); $date = $wpdb->get_var( "SELECT DATE_ADD(post_{$field}_gmt, INTERVAL '$add_seconds_server' SECOND) FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; } if ( $date ) { wp_cache_set( $key, $date, 'timeinfo' ); return $date; } return false; } /** * Updates posts in cache. * * @since 1.5.1 * * @param WP_Post[] $posts Array of post objects (passed by reference). */ function update_post_cache( &$posts ) { if ( ! $posts ) { return; } $data = array(); foreach ( $posts as $post ) { if ( empty( $post->filter ) || 'raw' !== $post->filter ) { $post = sanitize_post( $post, 'raw' ); } $data[ $post->ID ] = $post; } wp_cache_add_multiple( $data, 'posts' ); } /** * Will clean the post in the cache. * * Cleaning means delete from the cache of the post. Will call to clean the term * object cache associated with the post ID. * * This function not run if $_wp_suspend_cache_invalidation is not empty. See * wp_suspend_cache_invalidation(). * * @since 2.0.0 * * @global bool $_wp_suspend_cache_invalidation * * @param int|WP_Post $post Post ID or post object to remove from the cache. */ function clean_post_cache( $post ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } $post = get_post( $post ); if ( ! $post ) { return; } wp_cache_delete( $post->ID, 'posts' ); wp_cache_delete( 'post_parent:' . (string) $post->ID, 'posts' ); wp_cache_delete( $post->ID, 'post_meta' ); clean_object_term_cache( $post->ID, $post->post_type ); wp_cache_delete( 'wp_get_archives', 'general' ); /** * Fires immediately after the given post's cache is cleaned. * * @since 2.5.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ do_action( 'clean_post_cache', $post->ID, $post ); if ( 'page' === $post->post_type ) { wp_cache_delete( 'all_page_ids', 'posts' ); /** * Fires immediately after the given page's cache is cleaned. * * @since 2.5.0 * * @param int $post_id Post ID. */ do_action( 'clean_page_cache', $post->ID ); } wp_cache_set_posts_last_changed(); } /** * Updates post, term, and metadata caches for a list of post objects. * * @since 1.5.0 * * @param WP_Post[] $posts Array of post objects (passed by reference). * @param string $post_type Optional. Post type. Default 'post'. * @param bool $update_term_cache Optional. Whether to update the term cache. Default true. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function update_post_caches( &$posts, $post_type = 'post', $update_term_cache = true, $update_meta_cache = true ) { // No point in doing all this work if we didn't match any posts. if ( ! $posts ) { return; } update_post_cache( $posts ); $post_ids = array(); foreach ( $posts as $post ) { $post_ids[] = $post->ID; } if ( ! $post_type ) { $post_type = 'any'; } if ( $update_term_cache ) { if ( is_array( $post_type ) ) { $ptypes = $post_type; } elseif ( 'any' === $post_type ) { $ptypes = array(); // Just use the post_types in the supplied posts. foreach ( $posts as $post ) { $ptypes[] = $post->post_type; } $ptypes = array_unique( $ptypes ); } else { $ptypes = array( $post_type ); } if ( ! empty( $ptypes ) ) { update_object_term_cache( $post_ids, $ptypes ); } } if ( $update_meta_cache ) { update_postmeta_cache( $post_ids ); } } /** * Updates post author user caches for a list of post objects. * * @since 6.1.0 * * @param WP_Post[] $posts Array of post objects. */ function update_post_author_caches( $posts ) { /* * cache_users() is a pluggable function so is not available prior * to the `plugins_loaded` hook firing. This is to ensure against * fatal errors when the function is not available. */ if ( ! function_exists( 'cache_users' ) ) { return; } $author_ids = wp_list_pluck( $posts, 'post_author' ); $author_ids = array_map( 'absint', $author_ids ); $author_ids = array_unique( array_filter( $author_ids ) ); cache_users( $author_ids ); } /** * Updates parent post caches for a list of post objects. * * @since 6.1.0 * * @param WP_Post[] $posts Array of post objects. */ function update_post_parent_caches( $posts ) { $parent_ids = wp_list_pluck( $posts, 'post_parent' ); $parent_ids = array_map( 'absint', $parent_ids ); $parent_ids = array_unique( array_filter( $parent_ids ) ); if ( ! empty( $parent_ids ) ) { _prime_post_caches( $parent_ids, false ); } } /** * Updates metadata cache for a list of post IDs. * * Performs SQL query to retrieve the metadata for the post IDs and updates the * metadata cache for the posts. Therefore, the functions, which call this * function, do not need to perform SQL queries on their own. * * @since 2.1.0 * * @param int[] $post_ids Array of post IDs. * @return array|false An array of metadata on success, false if there is nothing to update. */ function update_postmeta_cache( $post_ids ) { return update_meta_cache( 'post', $post_ids ); } /** * Will clean the attachment in the cache. * * Cleaning means delete from the cache. Optionally will clean the term * object cache associated with the attachment ID. * * This function will not run if $_wp_suspend_cache_invalidation is not empty. * * @since 3.0.0 * * @global bool $_wp_suspend_cache_invalidation * * @param int $id The attachment ID in the cache to clean. * @param bool $clean_terms Optional. Whether to clean terms cache. Default false. */ function clean_attachment_cache( $id, $clean_terms = false ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } $id = (int) $id; wp_cache_delete( $id, 'posts' ); wp_cache_delete( $id, 'post_meta' ); if ( $clean_terms ) { clean_object_term_cache( $id, 'attachment' ); } /** * Fires after the given attachment's cache is cleaned. * * @since 3.0.0 * * @param int $id Attachment ID. */ do_action( 'clean_attachment_cache', $id ); } // // Hooks. // /** * Hook for managing future post transitions to published. * * @since 2.3.0 * @access private * * @see wp_clear_scheduled_hook() * @global wpdb $wpdb WordPress database abstraction object. * * @param string $new_status New post status. * @param string $old_status Previous post status. * @param WP_Post $post Post object. */ function _transition_post_status( $new_status, $old_status, $post ) { global $wpdb; if ( 'publish' !== $old_status && 'publish' === $new_status ) { // Reset GUID if transitioning to publish and it is empty. if ( '' === get_the_guid( $post->ID ) ) { $wpdb->update( $wpdb->posts, array( 'guid' => get_permalink( $post->ID ) ), array( 'ID' => $post->ID ) ); } /** * Fires when a post's status is transitioned from private to published. * * @since 1.5.0 * @deprecated 2.3.0 Use {@see 'private_to_publish'} instead. * * @param int $post_id Post ID. */ do_action_deprecated( 'private_to_published', array( $post->ID ), '2.3.0', 'private_to_publish' ); } // If published posts changed clear the lastpostmodified cache. if ( 'publish' === $new_status || 'publish' === $old_status ) { foreach ( array( 'server', 'gmt', 'blog' ) as $timezone ) { wp_cache_delete( "lastpostmodified:$timezone", 'timeinfo' ); wp_cache_delete( "lastpostdate:$timezone", 'timeinfo' ); wp_cache_delete( "lastpostdate:$timezone:{$post->post_type}", 'timeinfo' ); } } if ( $new_status !== $old_status ) { wp_cache_delete( _count_posts_cache_key( $post->post_type ), 'counts' ); wp_cache_delete( _count_posts_cache_key( $post->post_type, 'readable' ), 'counts' ); } // Always clears the hook in case the post status bounced from future to draft. wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); } /** * Hook used to schedule publication for a post marked for the future. * * The $post properties used and must exist are 'ID' and 'post_date_gmt'. * * @since 2.3.0 * @access private * * @param int $deprecated Not used. Can be set to null. Never implemented. Not marked * as deprecated with _deprecated_argument() as it conflicts with * wp_transition_post_status() and the default filter for _future_post_hook(). * @param WP_Post $post Post object. */ function _future_post_hook( $deprecated, $post ) { wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); wp_schedule_single_event( strtotime( get_gmt_from_date( $post->post_date ) . ' GMT' ), 'publish_future_post', array( $post->ID ) ); } /** * Hook to schedule pings and enclosures when a post is published. * * Uses XMLRPC_REQUEST and WP_IMPORTING constants. * * @since 2.3.0 * @access private * * @param int $post_id The ID of the post being published. */ function _publish_post_hook( $post_id ) { if ( defined( 'XMLRPC_REQUEST' ) ) { /** * Fires when _publish_post_hook() is called during an XML-RPC request. * * @since 2.1.0 * * @param int $post_id Post ID. */ do_action( 'xmlrpc_publish_post', $post_id ); } if ( defined( 'WP_IMPORTING' ) ) { return; } if ( get_option( 'default_pingback_flag' ) ) { add_post_meta( $post_id, '_pingme', '1', true ); } add_post_meta( $post_id, '_encloseme', '1', true ); $to_ping = get_to_ping( $post_id ); if ( ! empty( $to_ping ) ) { add_post_meta( $post_id, '_trackbackme', '1' ); } if ( ! wp_next_scheduled( 'do_pings' ) ) { wp_schedule_single_event( time(), 'do_pings' ); } } /** * Returns the ID of the post's parent. * * @since 3.1.0 * @since 5.9.0 The `$post` parameter was made optional. * * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post. * @return int|false Post parent ID (which can be 0 if there is no parent), * or false if the post does not exist. */ function wp_get_post_parent_id( $post = null ) { $post = get_post( $post ); if ( ! $post || is_wp_error( $post ) ) { return false; } return (int) $post->post_parent; } /** * Checks the given subset of the post hierarchy for hierarchy loops. * * Prevents loops from forming and breaks those that it finds. Attached * to the {@see 'wp_insert_post_parent'} filter. * * @since 3.1.0 * * @see wp_find_hierarchy_loop() * * @param int $post_parent ID of the parent for the post we're checking. * @param int $post_id ID of the post we're checking. * @return int The new post_parent for the post, 0 otherwise. */ function wp_check_post_hierarchy_for_loops( $post_parent, $post_id ) { // Nothing fancy here - bail. if ( ! $post_parent ) { return 0; } // New post can't cause a loop. if ( ! $post_id ) { return $post_parent; } // Can't be its own parent. if ( $post_parent === $post_id ) { return 0; } // Now look for larger loops. $loop = wp_find_hierarchy_loop( 'wp_get_post_parent_id', $post_id, $post_parent ); if ( ! $loop ) { return $post_parent; // No loop. } // Setting $post_parent to the given value causes a loop. if ( isset( $loop[ $post_id ] ) ) { return 0; } // There's a loop, but it doesn't contain $post_id. Break the loop. foreach ( array_keys( $loop ) as $loop_member ) { wp_update_post( array( 'ID' => $loop_member, 'post_parent' => 0, ) ); } return $post_parent; } /** * Sets the post thumbnail (featured image) for the given post. * * @since 3.1.0 * * @param int|WP_Post $post Post ID or post object where thumbnail should be attached. * @param int $thumbnail_id Thumbnail to attach. * @return int|bool Post meta ID if the key didn't exist (ie. this is the first time that * a thumbnail has been saved for the post), true on successful update, * false on failure or if the value passed is the same as the one that * is already in the database. */ function set_post_thumbnail( $post, $thumbnail_id ) { $post = get_post( $post ); $thumbnail_id = absint( $thumbnail_id ); if ( $post && $thumbnail_id && get_post( $thumbnail_id ) ) { if ( wp_get_attachment_image( $thumbnail_id, 'thumbnail' ) ) { return update_post_meta( $post->ID, '_thumbnail_id', $thumbnail_id ); } else { return delete_post_meta( $post->ID, '_thumbnail_id' ); } } return false; } /** * Removes the thumbnail (featured image) from the given post. * * @since 3.3.0 * * @param int|WP_Post $post Post ID or post object from which the thumbnail should be removed. * @return bool True on success, false on failure. */ function delete_post_thumbnail( $post ) { $post = get_post( $post ); if ( $post ) { return delete_post_meta( $post->ID, '_thumbnail_id' ); } return false; } /** * Deletes auto-drafts for new posts that are > 7 days old. * * @since 3.4.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function wp_delete_auto_drafts() { global $wpdb; // Cleanup old auto-drafts more than 7 days old. $old_posts = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'auto-draft' AND DATE_SUB( NOW(), INTERVAL 7 DAY ) > post_date" ); foreach ( (array) $old_posts as $delete ) { // Force delete. wp_delete_post( $delete, true ); } } /** * Queues posts for lazy-loading of term meta. * * @since 4.5.0 * * @param WP_Post[] $posts Array of WP_Post objects. */ function wp_queue_posts_for_term_meta_lazyload( $posts ) { $post_type_taxonomies = array(); $prime_post_terms = array(); foreach ( $posts as $post ) { if ( ! ( $post instanceof WP_Post ) ) { continue; } if ( ! isset( $post_type_taxonomies[ $post->post_type ] ) ) { $post_type_taxonomies[ $post->post_type ] = get_object_taxonomies( $post->post_type ); } foreach ( $post_type_taxonomies[ $post->post_type ] as $taxonomy ) { $prime_post_terms[ $taxonomy ][] = $post->ID; } } $term_ids = array(); if ( $prime_post_terms ) { foreach ( $prime_post_terms as $taxonomy => $post_ids ) { $cached_term_ids = wp_cache_get_multiple( $post_ids, "{$taxonomy}_relationships" ); if ( is_array( $cached_term_ids ) ) { $cached_term_ids = array_filter( $cached_term_ids ); foreach ( $cached_term_ids as $_term_ids ) { // Backward compatibility for if a plugin is putting objects into the cache, rather than IDs. foreach ( $_term_ids as $term_id ) { if ( is_numeric( $term_id ) ) { $term_ids[] = (int) $term_id; } elseif ( isset( $term_id->term_id ) ) { $term_ids[] = (int) $term_id->term_id; } } } } } $term_ids = array_unique( $term_ids ); } wp_lazyload_term_meta( $term_ids ); } /** * Updates the custom taxonomies' term counts when a post's status is changed. * * For example, default posts term counts (for custom taxonomies) don't include * private / draft posts. * * @since 3.3.0 * @access private * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. */ function _update_term_count_on_transition_post_status( $new_status, $old_status, $post ) { // Update counts for the post's terms. foreach ( (array) get_object_taxonomies( $post->post_type ) as $taxonomy ) { $tt_ids = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'tt_ids' ) ); wp_update_term_count( $tt_ids, $taxonomy ); } } /** * Adds any posts from the given IDs to the cache that do not already exist in cache. * * @since 3.4.0 * @since 6.1.0 This function is no longer marked as "private". * * @see update_post_cache() * @see update_postmeta_cache() * @see update_object_term_cache() * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $ids ID list. * @param bool $update_term_cache Optional. Whether to update the term cache. Default true. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function _prime_post_caches( $ids, $update_term_cache = true, $update_meta_cache = true ) { global $wpdb; $non_cached_ids = _get_non_cached_ids( $ids, 'posts' ); if ( ! empty( $non_cached_ids ) ) { $fresh_posts = $wpdb->get_results( sprintf( "SELECT $wpdb->posts.* FROM $wpdb->posts WHERE ID IN (%s)", implode( ',', $non_cached_ids ) ) ); if ( $fresh_posts ) { // Despite the name, update_post_cache() expects an array rather than a single post. update_post_cache( $fresh_posts ); } } if ( $update_meta_cache ) { update_postmeta_cache( $ids ); } if ( $update_term_cache ) { $post_types = array_map( 'get_post_type', $ids ); $post_types = array_unique( $post_types ); update_object_term_cache( $ids, $post_types ); } } /** * Prime the cache containing the parent ID of various post objects. * * @since 6.4.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $ids ID list. */ function _prime_post_parent_id_caches( array $ids ) { global $wpdb; $ids = array_filter( $ids, '_validate_cache_id' ); $ids = array_unique( array_map( 'intval', $ids ), SORT_NUMERIC ); if ( empty( $ids ) ) { return; } $cache_keys = array(); foreach ( $ids as $id ) { $cache_keys[ $id ] = 'post_parent:' . (string) $id; } $cached_data = wp_cache_get_multiple( array_values( $cache_keys ), 'posts' ); $non_cached_ids = array(); foreach ( $cache_keys as $id => $cache_key ) { if ( false === $cached_data[ $cache_key ] ) { $non_cached_ids[] = $id; } } if ( ! empty( $non_cached_ids ) ) { $fresh_posts = $wpdb->get_results( sprintf( "SELECT $wpdb->posts.ID, $wpdb->posts.post_parent FROM $wpdb->posts WHERE ID IN (%s)", implode( ',', $non_cached_ids ) ) ); if ( $fresh_posts ) { $post_parent_data = array(); foreach ( $fresh_posts as $fresh_post ) { $post_parent_data[ 'post_parent:' . (string) $fresh_post->ID ] = (int) $fresh_post->post_parent; } wp_cache_add_multiple( $post_parent_data, 'posts' ); } } } /** * Adds a suffix if any trashed posts have a given slug. * * Store its desired (i.e. current) slug so it can try to reclaim it * if the post is untrashed. * * For internal use. * * @since 4.5.0 * @access private * * @param string $post_name Post slug. * @param int $post_id Optional. Post ID that should be ignored. Default 0. */ function wp_add_trashed_suffix_to_post_name_for_trashed_posts( $post_name, $post_id = 0 ) { $trashed_posts_with_desired_slug = get_posts( array( 'name' => $post_name, 'post_status' => 'trash', 'post_type' => 'any', 'nopaging' => true, 'post__not_in' => array( $post_id ), ) ); if ( ! empty( $trashed_posts_with_desired_slug ) ) { foreach ( $trashed_posts_with_desired_slug as $_post ) { wp_add_trashed_suffix_to_post_name_for_post( $_post ); } } } /** * Adds a trashed suffix for a given post. * * Store its desired (i.e. current) slug so it can try to reclaim it * if the post is untrashed. * * For internal use. * * @since 4.5.0 * @access private * * @global wpdb $wpdb WordPress database abstraction object. * * @param WP_Post $post The post. * @return string New slug for the post. */ function wp_add_trashed_suffix_to_post_name_for_post( $post ) { global $wpdb; $post = get_post( $post ); if ( str_ends_with( $post->post_name, '__trashed' ) ) { return $post->post_name; } add_post_meta( $post->ID, '_wp_desired_post_slug', $post->post_name ); $post_name = _truncate_post_slug( $post->post_name, 191 ) . '__trashed'; $wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) ); clean_post_cache( $post->ID ); return $post_name; } /** * Sets the last changed time for the 'posts' cache group. * * @since 5.0.0 */ function wp_cache_set_posts_last_changed() { wp_cache_set_last_changed( 'posts' ); } /** * Gets all available post MIME types for a given post type. * * @since 2.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $type * @return string[] An array of MIME types. */ function get_available_post_mime_types( $type = 'attachment' ) { global $wpdb; /** * Filters the list of available post MIME types for the given post type. * * @since 6.4.0 * * @param string[]|null $mime_types An array of MIME types. Default null. * @param string $type The post type name. Usually 'attachment' but can be any post type. */ $mime_types = apply_filters( 'pre_get_available_post_mime_types', null, $type ); if ( ! is_array( $mime_types ) ) { $mime_types = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT post_mime_type FROM $wpdb->posts WHERE post_type = %s AND post_mime_type != ''", $type ) ); } // Remove nulls from returned $mime_types. return array_values( array_filter( $mime_types ) ); } /** * Retrieves the path to an uploaded image file. * * Similar to `get_attached_file()` however some images may have been processed after uploading * to make them suitable for web use. In this case the attached "full" size file is usually replaced * with a scaled down version of the original image. This function always returns the path * to the originally uploaded image file. * * @since 5.3.0 * @since 5.4.0 Added the `$unfiltered` parameter. * * @param int $attachment_id Attachment ID. * @param bool $unfiltered Optional. Passed through to `get_attached_file()`. Default false. * @return string|false Path to the original image file or false if the attachment is not an image. */ function wp_get_original_image_path( $attachment_id, $unfiltered = false ) { if ( ! wp_attachment_is_image( $attachment_id ) ) { return false; } $image_meta = wp_get_attachment_metadata( $attachment_id ); $image_file = get_attached_file( $attachment_id, $unfiltered ); if ( empty( $image_meta['original_image'] ) ) { $original_image = $image_file; } else { $original_image = path_join( dirname( $image_file ), $image_meta['original_image'] ); } /** * Filters the path to the original image. * * @since 5.3.0 * * @param string $original_image Path to original image file. * @param int $attachment_id Attachment ID. */ return apply_filters( 'wp_get_original_image_path', $original_image, $attachment_id ); } /** * Retrieves the URL to an original attachment image. * * Similar to `wp_get_attachment_url()` however some images may have been * processed after uploading. In this case this function returns the URL * to the originally uploaded image file. * * @since 5.3.0 * * @param int $attachment_id Attachment post ID. * @return string|false Attachment image URL, false on error or if the attachment is not an image. */ function wp_get_original_image_url( $attachment_id ) { if ( ! wp_attachment_is_image( $attachment_id ) ) { return false; } $image_url = wp_get_attachment_url( $attachment_id ); if ( ! $image_url ) { return false; } $image_meta = wp_get_attachment_metadata( $attachment_id ); if ( empty( $image_meta['original_image'] ) ) { $original_image_url = $image_url; } else { $original_image_url = path_join( dirname( $image_url ), $image_meta['original_image'] ); } /** * Filters the URL to the original attachment image. * * @since 5.3.0 * * @param string $original_image_url URL to original image. * @param int $attachment_id Attachment ID. */ return apply_filters( 'wp_get_original_image_url', $original_image_url, $attachment_id ); } /** * Filters callback which sets the status of an untrashed post to its previous status. * * This can be used as a callback on the `wp_untrash_post_status` filter. * * @since 5.6.0 * * @param string $new_status The new status of the post being restored. * @param int $post_id The ID of the post being restored. * @param string $previous_status The status of the post at the point where it was trashed. * @return string The new status of the post. */ function wp_untrash_post_set_previous_status( $new_status, $post_id, $previous_status ) { return $previous_status; } /** * Returns whether the post can be edited in the block editor. * * @since 5.0.0 * @since 6.1.0 Moved to wp-includes from wp-admin. * * @param int|WP_Post $post Post ID or WP_Post object. * @return bool Whether the post can be edited in the block editor. */ function use_block_editor_for_post( $post ) { $post = get_post( $post ); if ( ! $post ) { return false; } // We're in the meta box loader, so don't use the block editor. if ( is_admin() && isset( $_GET['meta-box-loader'] ) ) { check_admin_referer( 'meta-box-loader', 'meta-box-loader-nonce' ); return false; } $use_block_editor = use_block_editor_for_post_type( $post->post_type ); /** * Filters whether a post is able to be edited in the block editor. * * @since 5.0.0 * * @param bool $use_block_editor Whether the post can be edited or not. * @param WP_Post $post The post being checked. */ return apply_filters( 'use_block_editor_for_post', $use_block_editor, $post ); } /** * Returns whether a post type is compatible with the block editor. * * The block editor depends on the REST API, and if the post type is not shown in the * REST API, then it won't work with the block editor. * * @since 5.0.0 * @since 6.1.0 Moved to wp-includes from wp-admin. * * @param string $post_type The post type. * @return bool Whether the post type can be edited with the block editor. */ function use_block_editor_for_post_type( $post_type ) { if ( ! post_type_exists( $post_type ) ) { return false; } if ( ! post_type_supports( $post_type, 'editor' ) ) { return false; } $post_type_object = get_post_type_object( $post_type ); if ( $post_type_object && ! $post_type_object->show_in_rest ) { return false; } /** * Filters whether a post is able to be edited in the block editor. * * @since 5.0.0 * * @param bool $use_block_editor Whether the post type can be edited or not. Default true. * @param string $post_type The post type being checked. */ return apply_filters( 'use_block_editor_for_post_type', true, $post_type ); } /** * Registers any additional post meta fields. * * @since 6.3.0 Adds `wp_pattern_sync_status` meta field to the wp_block post type so an unsynced option can be added. * * @link https://github.com/WordPress/gutenberg/pull/51144 */ function wp_create_initial_post_meta() { register_post_meta( 'wp_block', 'wp_pattern_sync_status', array( 'sanitize_callback' => 'sanitize_text_field', 'single' => true, 'type' => 'string', 'show_in_rest' => array( 'schema' => array( 'type' => 'string', 'enum' => array( 'partial', 'unsynced' ), ), ), ) ); } array( 'unknown' => _x( 'Password strength unknown', 'password strength' ), 'short' => _x( 'Very weak', 'password strength' ), 'bad' => _x( 'Weak', 'password strength' ), 'good' => _x( 'Medium', 'password strength' ), 'strong' => _x( 'Strong', 'password strength' ), 'mismatch' => _x( 'Mismatch', 'password mismatch' ), ) ); $scripts->set_translations( 'password-strength-meter' ); $scripts->add( 'password-toggle', "/wp-admin/js/password-toggle$suffix.js", array(), false, 1 ); $scripts->set_translations( 'password-toggle' ); $scripts->add( 'application-passwords', "/wp-admin/js/application-passwords$suffix.js", array( 'jquery', 'wp-util', 'wp-api-request', 'wp-date', 'wp-i18n', 'wp-hooks' ), false, 1 ); $scripts->set_translations( 'application-passwords' ); $scripts->add( 'auth-app', "/wp-admin/js/auth-app$suffix.js", array( 'jquery', 'wp-api-request', 'wp-i18n', 'wp-hooks' ), false, 1 ); $scripts->set_translations( 'auth-app' ); $scripts->add( 'user-profile', "/wp-admin/js/user-profile$suffix.js", array( 'clipboard', 'jquery', 'password-strength-meter', 'wp-util', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'user-profile' ); $user_id = isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : 0; did_action( 'init' ) && $scripts->localize( 'user-profile', 'userProfileL10n', array( 'user_id' => $user_id, 'nonce' => wp_installing() ? '' : wp_create_nonce( 'reset-password-for-' . $user_id ), ) ); $scripts->add( 'language-chooser', "/wp-admin/js/language-chooser$suffix.js", array( 'jquery' ), false, 1 ); $scripts->add( 'user-suggest', "/wp-admin/js/user-suggest$suffix.js", array( 'jquery-ui-autocomplete' ), false, 1 ); $scripts->add( 'admin-bar', "/wp-includes/js/admin-bar$suffix.js", array( 'hoverintent-js' ), false, 1 ); $scripts->add( 'wplink', "/wp-includes/js/wplink$suffix.js", array( 'common', 'jquery', 'wp-a11y', 'wp-i18n' ), false, 1 ); $scripts->set_translations( 'wplink' ); did_action( 'init' ) && $scripts->localize( 'wplink', 'wpLinkL10n', array( 'title' => __( 'Insert/edit link' ), 'update' => __( 'Update' ), 'save' => __( 'Add Link' ), 'noTitle' => __( '(no title)' ), 'noMatchesFound' => __( 'No results found.' ), 'linkSelected' => __( 'Link selected.' ), 'linkInserted' => __( 'Link inserted.' ), /* translators: Minimum input length in characters to start searching posts in the "Insert/edit link" modal. */ 'minInputLength' => (int) _x( '3', 'minimum input length for searching post links' ), ) ); $scripts->add( 'wpdialogs', "/wp-includes/js/wpdialog$suffix.js", array( 'jquery-ui-dialog' ), false, 1 ); $scripts->add( 'word-count', "/wp-admin/js/word-count$suffix.js", array(), false, 1 ); $scripts->add( 'media-upload', "/wp-admin/js/media-upload$suffix.js", array( 'thickbox', 'shortcode' ), false, 1 ); $scripts->add( 'hoverIntent', "/wp-includes/js/hoverIntent$suffix.js", array( 'jquery' ), '1.10.2', 1 ); // JS-only version of hoverintent (no dependencies). $scripts->add( 'hoverintent-js', '/wp-includes/js/hoverintent-js.min.js', array(), '2.2.1', 1 ); $scripts->add( 'customize-base', "/wp-includes/js/customize-base$suffix.js", array( 'jquery', 'json2', 'underscore' ), false, 1 ); $scripts->add( 'customize-loader', "/wp-includes/js/customize-loader$suffix.js", array( 'customize-base' ), false, 1 ); $scripts->add( 'customize-preview', "/wp-includes/js/customize-preview$suffix.js", array( 'wp-a11y', 'customize-base' ), false, 1 ); $scripts->add( 'customize-models', '/wp-includes/js/customize-models.js', array( 'underscore', 'backbone' ), false, 1 ); $scripts->add( 'customize-views', '/wp-includes/js/customize-views.js', array( 'jquery', 'underscore', 'imgareaselect', 'customize-models', 'media-editor', 'media-views' ), false, 1 ); $scripts->add( 'customize-controls', "/wp-admin/js/customize-controls$suffix.js", array( 'customize-base', 'wp-a11y', 'wp-util', 'jquery-ui-core' ), false, 1 ); did_action( 'init' ) && $scripts->localize( 'customize-controls', '_wpCustomizeControlsL10n', array( 'activate' => __( 'Activate & Publish' ), 'save' => __( 'Save & Publish' ), // @todo Remove as not required. 'publish' => __( 'Publish' ), 'published' => __( 'Published' ), 'saveDraft' => __( 'Save Draft' ), 'draftSaved' => __( 'Draft Saved' ), 'updating' => __( 'Updating' ), 'schedule' => _x( 'Schedule', 'customizer changeset action/button label' ), 'scheduled' => _x( 'Scheduled', 'customizer changeset status' ), 'invalid' => __( 'Invalid' ), 'saveBeforeShare' => __( 'Please save your changes in order to share the preview.' ), 'futureDateError' => __( 'You must supply a future date to schedule.' ), 'saveAlert' => __( 'The changes you made will be lost if you navigate away from this page.' ), 'saved' => __( 'Saved' ), 'cancel' => __( 'Cancel' ), 'close' => __( 'Close' ), 'action' => __( 'Action' ), 'discardChanges' => __( 'Discard changes' ), 'cheatin' => __( 'An error occurred. Please try again later.' ), 'notAllowedHeading' => __( 'You need a higher level of permission.' ), 'notAllowed' => __( 'Sorry, you are not allowed to customize this site.' ), 'previewIframeTitle' => __( 'Site Preview' ), 'loginIframeTitle' => __( 'Session expired' ), 'collapseSidebar' => _x( 'Hide Controls', 'label for hide controls button without length constraints' ), 'expandSidebar' => _x( 'Show Controls', 'label for hide controls button without length constraints' ), 'untitledBlogName' => __( '(Untitled)' ), 'unknownRequestFail' => __( 'Looks like something’s gone wrong. Wait a couple seconds, and then try again.' ), 'themeDownloading' => __( 'Downloading your new theme…' ), 'themePreviewWait' => __( 'Setting up your live preview. This may take a bit.' ), 'revertingChanges' => __( 'Reverting unpublished changes…' ), 'trashConfirm' => __( 'Are you sure you want to discard your unpublished changes?' ), /* translators: %s: Display name of the user who has taken over the changeset in customizer. */ 'takenOverMessage' => __( '%s has taken over and is currently customizing.' ), /* translators: %s: URL to the Customizer to load the autosaved version. */ 'autosaveNotice' => __( 'There is a more recent autosave of your changes than the one you are previewing. Restore the autosave' ), 'videoHeaderNotice' => __( 'This theme does not support video headers on this page. Navigate to the front page or another page that supports video headers.' ), // Used for overriding the file types allowed in Plupload. 'allowedFiles' => __( 'Allowed Files' ), 'customCssError' => array( /* translators: %d: Error count. */ 'singular' => _n( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.', 1 ), /* translators: %d: Error count. */ 'plural' => _n( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.', 2 ), // @todo This is lacking, as some languages have a dedicated dual form. For proper handling of plurals in JS, see #20491. ), 'pageOnFrontError' => __( 'Homepage and posts page must be different.' ), 'saveBlockedError' => array( /* translators: %s: Number of invalid settings. */ 'singular' => _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', 1 ), /* translators: %s: Number of invalid settings. */ 'plural' => _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', 2 ), // @todo This is lacking, as some languages have a dedicated dual form. For proper handling of plurals in JS, see #20491. ), 'scheduleDescription' => __( 'Schedule your customization changes to publish ("go live") at a future date.' ), 'themePreviewUnavailable' => __( 'Sorry, you cannot preview new themes when you have changes scheduled or saved as a draft. Please publish your changes, or wait until they publish to preview new themes.' ), 'themeInstallUnavailable' => sprintf( /* translators: %s: URL to Add Themes admin screen. */ __( 'You will not be able to install new themes from here yet since your install requires SFTP credentials. For now, please add themes in the admin.' ), esc_url( admin_url( 'theme-install.php' ) ) ), 'publishSettings' => __( 'Publish Settings' ), 'invalidDate' => __( 'Invalid date.' ), 'invalidValue' => __( 'Invalid value.' ), 'blockThemeNotification' => sprintf( /* translators: 1: Link to Site Editor documentation on HelpHub, 2: HTML button. */ __( 'Hurray! Your theme supports site editing with blocks. Tell me more. %2$s' ), __( 'https://wordpress.org/documentation/article/site-editor/' ), sprintf( '', esc_url( admin_url( 'site-editor.php' ) ), __( 'Use Site Editor' ) ) ), ) ); $scripts->add( 'customize-selective-refresh', "/wp-includes/js/customize-selective-refresh$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 ); $scripts->add( 'customize-widgets', "/wp-admin/js/customize-widgets$suffix.js", array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-droppable', 'wp-backbone', 'customize-controls' ), false, 1 ); $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview', 'customize-selective-refresh' ), false, 1 ); $scripts->add( 'customize-nav-menus', "/wp-admin/js/customize-nav-menus$suffix.js", array( 'jquery', 'wp-backbone', 'customize-controls', 'accordion', 'nav-menu', 'wp-sanitize' ), false, 1 ); $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview', 'customize-selective-refresh' ), false, 1 ); $scripts->add( 'wp-custom-header', "/wp-includes/js/wp-custom-header$suffix.js", array( 'wp-a11y' ), false, 1 ); $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 ); $scripts->add( 'shortcode', "/wp-includes/js/shortcode$suffix.js", array( 'underscore' ), false, 1 ); $scripts->add( 'media-models', "/wp-includes/js/media-models$suffix.js", array( 'wp-backbone' ), false, 1 ); did_action( 'init' ) && $scripts->localize( 'media-models', '_wpMediaModelsL10n', array( 'settings' => array( 'ajaxurl' => admin_url( 'admin-ajax.php', 'relative' ), 'post' => array( 'id' => 0 ), ), ) ); $scripts->add( 'wp-embed', "/wp-includes/js/wp-embed$suffix.js" ); did_action( 'init' ) && $scripts->add_data( 'wp-embed', 'strategy', 'defer' ); /* * To enqueue media-views or media-editor, call wp_enqueue_media(). * Both rely on numerous settings, styles, and templates to operate correctly. */ $scripts->add( 'media-views', "/wp-includes/js/media-views$suffix.js", array( 'utils', 'media-models', 'wp-plupload', 'jquery-ui-sortable', 'wp-mediaelement', 'wp-api-request', 'wp-a11y', 'clipboard' ), false, 1 ); $scripts->set_translations( 'media-views' ); $scripts->add( 'media-editor', "/wp-includes/js/media-editor$suffix.js", array( 'shortcode', 'media-views' ), false, 1 ); $scripts->set_translations( 'media-editor' ); $scripts->add( 'media-audiovideo', "/wp-includes/js/media-audiovideo$suffix.js", array( 'media-editor' ), false, 1 ); $scripts->add( 'mce-view', "/wp-includes/js/mce-view$suffix.js", array( 'shortcode', 'jquery', 'media-views', 'media-audiovideo' ), false, 1 ); $scripts->add( 'wp-api', "/wp-includes/js/wp-api$suffix.js", array( 'jquery', 'backbone', 'underscore', 'wp-api-request' ), false, 1 ); if ( is_admin() ) { $scripts->add( 'admin-tags', "/wp-admin/js/tags$suffix.js", array( 'jquery', 'wp-ajax-response' ), false, 1 ); $scripts->set_translations( 'admin-tags' ); $scripts->add( 'admin-comments', "/wp-admin/js/edit-comments$suffix.js", array( 'wp-lists', 'quicktags', 'jquery-query', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'admin-comments' ); did_action( 'init' ) && $scripts->localize( 'admin-comments', 'adminCommentsSettings', array( 'hotkeys_highlight_first' => isset( $_GET['hotkeys_highlight_first'] ), 'hotkeys_highlight_last' => isset( $_GET['hotkeys_highlight_last'] ), ) ); $scripts->add( 'xfn', "/wp-admin/js/xfn$suffix.js", array( 'jquery' ), false, 1 ); $scripts->add( 'postbox', "/wp-admin/js/postbox$suffix.js", array( 'jquery-ui-sortable', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'postbox' ); $scripts->add( 'tags-box', "/wp-admin/js/tags-box$suffix.js", array( 'jquery', 'tags-suggest' ), false, 1 ); $scripts->set_translations( 'tags-box' ); $scripts->add( 'tags-suggest', "/wp-admin/js/tags-suggest$suffix.js", array( 'common', 'jquery-ui-autocomplete', 'wp-a11y', 'wp-i18n' ), false, 1 ); $scripts->set_translations( 'tags-suggest' ); $scripts->add( 'post', "/wp-admin/js/post$suffix.js", array( 'suggest', 'wp-lists', 'postbox', 'tags-box', 'underscore', 'word-count', 'wp-a11y', 'wp-sanitize', 'clipboard' ), false, 1 ); $scripts->set_translations( 'post' ); $scripts->add( 'editor-expand', "/wp-admin/js/editor-expand$suffix.js", array( 'jquery', 'underscore' ), false, 1 ); $scripts->add( 'link', "/wp-admin/js/link$suffix.js", array( 'wp-lists', 'postbox' ), false, 1 ); $scripts->add( 'comment', "/wp-admin/js/comment$suffix.js", array( 'jquery', 'postbox' ), false, 1 ); $scripts->set_translations( 'comment' ); $scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) ); $scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'admin-widgets' ); $scripts->add( 'media-widgets', "/wp-admin/js/widgets/media-widgets$suffix.js", array( 'jquery', 'media-models', 'media-views', 'wp-api-request' ) ); $scripts->add_inline_script( 'media-widgets', 'wp.mediaWidgets.init();', 'after' ); $scripts->add( 'media-audio-widget', "/wp-admin/js/widgets/media-audio-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) ); $scripts->add( 'media-image-widget', "/wp-admin/js/widgets/media-image-widget$suffix.js", array( 'media-widgets' ) ); $scripts->add( 'media-gallery-widget', "/wp-admin/js/widgets/media-gallery-widget$suffix.js", array( 'media-widgets' ) ); $scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo', 'wp-api-request' ) ); $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-a11y' ) ); $scripts->add( 'custom-html-widgets', "/wp-admin/js/widgets/custom-html-widgets$suffix.js", array( 'jquery', 'backbone', 'wp-util', 'jquery-ui-core', 'wp-a11y' ) ); $scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y', 'customize-base' ), false, 1 ); $scripts->add( 'inline-edit-post', "/wp-admin/js/inline-edit-post$suffix.js", array( 'jquery', 'tags-suggest', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'inline-edit-post' ); $scripts->add( 'inline-edit-tax', "/wp-admin/js/inline-edit-tax$suffix.js", array( 'jquery', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'inline-edit-tax' ); $scripts->add( 'plugin-install', "/wp-admin/js/plugin-install$suffix.js", array( 'jquery', 'jquery-ui-core', 'thickbox' ), false, 1 ); $scripts->set_translations( 'plugin-install' ); $scripts->add( 'site-health', "/wp-admin/js/site-health$suffix.js", array( 'clipboard', 'jquery', 'wp-util', 'wp-a11y', 'wp-api-request', 'wp-url', 'wp-i18n', 'wp-hooks' ), false, 1 ); $scripts->set_translations( 'site-health' ); $scripts->add( 'privacy-tools', "/wp-admin/js/privacy-tools$suffix.js", array( 'jquery', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'privacy-tools' ); $scripts->add( 'updates', "/wp-admin/js/updates$suffix.js", array( 'common', 'jquery', 'wp-util', 'wp-a11y', 'wp-sanitize', 'wp-i18n' ), false, 1 ); $scripts->set_translations( 'updates' ); did_action( 'init' ) && $scripts->localize( 'updates', '_wpUpdatesSettings', array( 'ajax_nonce' => wp_installing() ? '' : wp_create_nonce( 'updates' ), ) ); $scripts->add( 'farbtastic', '/wp-admin/js/farbtastic.js', array( 'jquery' ), '1.2' ); $scripts->add( 'iris', '/wp-admin/js/iris.min.js', array( 'jquery-ui-draggable', 'jquery-ui-slider', 'jquery-touch-punch' ), '1.1.1', 1 ); $scripts->add( 'wp-color-picker', "/wp-admin/js/color-picker$suffix.js", array( 'iris' ), false, 1 ); $scripts->set_translations( 'wp-color-picker' ); $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y', 'wp-date' ), false, 1 ); $scripts->set_translations( 'dashboard' ); $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" ); $scripts->add( 'media-grid', "/wp-includes/js/media-grid$suffix.js", array( 'media-editor' ), false, 1 ); $scripts->add( 'media', "/wp-admin/js/media$suffix.js", array( 'jquery', 'clipboard', 'wp-i18n', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'media' ); $scripts->add( 'image-edit', "/wp-admin/js/image-edit$suffix.js", array( 'jquery', 'jquery-ui-core', 'json2', 'imgareaselect', 'wp-a11y' ), false, 1 ); $scripts->set_translations( 'image-edit' ); $scripts->add( 'set-post-thumbnail', "/wp-admin/js/set-post-thumbnail$suffix.js", array( 'jquery' ), false, 1 ); $scripts->set_translations( 'set-post-thumbnail' ); /* * Navigation Menus: Adding underscore as a dependency to utilize _.debounce * see https://core.trac.wordpress.org/ticket/42321 */ $scripts->add( 'nav-menu', "/wp-admin/js/nav-menu$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable', 'wp-lists', 'postbox', 'json2', 'underscore' ) ); $scripts->set_translations( 'nav-menu' ); $scripts->add( 'custom-header', '/wp-admin/js/custom-header.js', array( 'jquery-masonry' ), false, 1 ); $scripts->add( 'custom-background', "/wp-admin/js/custom-background$suffix.js", array( 'wp-color-picker', 'media-views' ), false, 1 ); $scripts->add( 'media-gallery', "/wp-admin/js/media-gallery$suffix.js", array( 'jquery' ), false, 1 ); $scripts->add( 'svg-painter', '/wp-admin/js/svg-painter.js', array( 'jquery' ), false, 1 ); } } /** * Assigns default styles to $styles object. * * Nothing is returned, because the $styles parameter is passed by reference. * Meaning that whatever object is passed will be updated without having to * reassign the variable that was passed back to the same value. This saves * memory. * * Adding default styles is not the only task, it also assigns the base_url * property, the default version, and text direction for the object. * * @since 2.6.0 * * @global array $editor_styles * * @param WP_Styles $styles */ function wp_default_styles( $styles ) { global $editor_styles; /* * Include an unmodified $wp_version. * * Note: wp_get_wp_version() is not used here, as this file can be included * via wp-admin/load-scripts.php or wp-admin/load-styles.php, in which case * wp-includes/functions.php is not loaded. */ require ABSPATH . WPINC . '/version.php'; if ( ! defined( 'SCRIPT_DEBUG' ) ) { /* * Note: str_contains() is not used here, as this file can be included * via wp-admin/load-scripts.php or wp-admin/load-styles.php, in which case * the polyfills from wp-includes/compat.php are not loaded. */ define( 'SCRIPT_DEBUG', false !== strpos( $wp_version, '-src' ) ); } $guessurl = site_url(); if ( ! $guessurl ) { $guessurl = wp_guess_url(); } $styles->base_url = $guessurl; $styles->content_url = defined( 'WP_CONTENT_URL' ) ? WP_CONTENT_URL : ''; $styles->default_version = get_bloginfo( 'version' ); $styles->text_direction = function_exists( 'is_rtl' ) && is_rtl() ? 'rtl' : 'ltr'; $styles->default_dirs = array( '/wp-admin/', '/wp-includes/css/' ); // Open Sans is no longer used by core, but may be relied upon by themes and plugins. $open_sans_font_url = ''; /* * translators: If there are characters in your language that are not supported * by Open Sans, translate this to 'off'. Do not translate into your own language. */ if ( 'off' !== _x( 'on', 'Open Sans font: on or off' ) ) { $subsets = 'latin,latin-ext'; /* * translators: To add an additional Open Sans character subset specific to your language, * translate this to 'greek', 'cyrillic' or 'vietnamese'. Do not translate into your own language. */ $subset = _x( 'no-subset', 'Open Sans font: add new subset (greek, cyrillic, vietnamese)' ); if ( 'cyrillic' === $subset ) { $subsets .= ',cyrillic,cyrillic-ext'; } elseif ( 'greek' === $subset ) { $subsets .= ',greek,greek-ext'; } elseif ( 'vietnamese' === $subset ) { $subsets .= ',vietnamese'; } // Hotlink Open Sans, for now. $open_sans_font_url = "https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,300,400,600&subset=$subsets&display=fallback"; } // Register a stylesheet for the selected admin color scheme. $styles->add( 'colors', true, array( 'wp-admin', 'buttons' ) ); $suffix = SCRIPT_DEBUG ? '' : '.min'; // Admin CSS. $styles->add( 'common', "/wp-admin/css/common$suffix.css" ); $styles->add( 'forms', "/wp-admin/css/forms$suffix.css" ); $styles->add( 'admin-menu', "/wp-admin/css/admin-menu$suffix.css" ); $styles->add( 'dashboard', "/wp-admin/css/dashboard$suffix.css" ); $styles->add( 'list-tables', "/wp-admin/css/list-tables$suffix.css" ); $styles->add( 'edit', "/wp-admin/css/edit$suffix.css" ); $styles->add( 'revisions', "/wp-admin/css/revisions$suffix.css" ); $styles->add( 'media', "/wp-admin/css/media$suffix.css" ); $styles->add( 'themes', "/wp-admin/css/themes$suffix.css" ); $styles->add( 'about', "/wp-admin/css/about$suffix.css" ); $styles->add( 'nav-menus', "/wp-admin/css/nav-menus$suffix.css" ); $styles->add( 'widgets', "/wp-admin/css/widgets$suffix.css", array( 'wp-pointer' ) ); $styles->add( 'site-icon', "/wp-admin/css/site-icon$suffix.css" ); $styles->add( 'l10n', "/wp-admin/css/l10n$suffix.css" ); $styles->add( 'code-editor', "/wp-admin/css/code-editor$suffix.css", array( 'wp-codemirror' ) ); $styles->add( 'site-health', "/wp-admin/css/site-health$suffix.css" ); $styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n' ) ); $styles->add( 'login', "/wp-admin/css/login$suffix.css", array( 'dashicons', 'buttons', 'forms', 'l10n' ) ); $styles->add( 'install', "/wp-admin/css/install$suffix.css", array( 'dashicons', 'buttons', 'forms', 'l10n' ) ); $styles->add( 'wp-color-picker', "/wp-admin/css/color-picker$suffix.css" ); $styles->add( 'customize-controls', "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'imgareaselect' ) ); $styles->add( 'customize-widgets', "/wp-admin/css/customize-widgets$suffix.css", array( 'wp-admin', 'colors' ) ); $styles->add( 'customize-nav-menus', "/wp-admin/css/customize-nav-menus$suffix.css", array( 'wp-admin', 'colors' ) ); // Common dependencies. $styles->add( 'buttons', "/wp-includes/css/buttons$suffix.css" ); $styles->add( 'dashicons', "/wp-includes/css/dashicons$suffix.css" ); // Includes CSS. $styles->add( 'admin-bar', "/wp-includes/css/admin-bar$suffix.css", array( 'dashicons' ) ); $styles->add( 'wp-auth-check', "/wp-includes/css/wp-auth-check$suffix.css", array( 'dashicons' ) ); $styles->add( 'editor-buttons', "/wp-includes/css/editor$suffix.css", array( 'dashicons' ) ); $styles->add( 'media-views', "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) ); $styles->add( 'wp-pointer', "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) ); $styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css", array( 'dashicons' ) ); $styles->add( 'wp-embed-template-ie', "/wp-includes/css/wp-embed-template-ie$suffix.css" ); $styles->add( 'wp-empty-template-alert', "/wp-includes/css/wp-empty-template-alert$suffix.css" ); $styles->add_data( 'wp-embed-template-ie', 'conditional', 'lte IE 8' ); // External libraries and friends. $styles->add( 'imgareaselect', '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.8' ); $styles->add( 'wp-jquery-ui-dialog', "/wp-includes/css/jquery-ui-dialog$suffix.css", array( 'dashicons' ) ); $styles->add( 'mediaelement', '/wp-includes/js/mediaelement/mediaelementplayer-legacy.min.css', array(), '4.2.17' ); $styles->add( 'wp-mediaelement', "/wp-includes/js/mediaelement/wp-mediaelement$suffix.css", array( 'mediaelement' ) ); $styles->add( 'thickbox', '/wp-includes/js/thickbox/thickbox.css', array( 'dashicons' ) ); $styles->add( 'wp-codemirror', '/wp-includes/js/codemirror/codemirror.min.css', array(), '5.29.1-alpha-ee20357' ); // Deprecated CSS. $styles->add( 'deprecated-media', "/wp-admin/css/deprecated-media$suffix.css" ); $styles->add( 'farbtastic', "/wp-admin/css/farbtastic$suffix.css", array(), '1.3u1' ); $styles->add( 'jcrop', '/wp-includes/js/jcrop/jquery.Jcrop.min.css', array(), '0.9.15' ); $styles->add( 'colors-fresh', false, array( 'wp-admin', 'buttons' ) ); // Old handle. $styles->add( 'open-sans', $open_sans_font_url ); // No longer used in core as of 4.6. // Noto Serif is no longer used by core, but may be relied upon by themes and plugins. $fonts_url = ''; /* * translators: Use this to specify the proper Google Font name and variants * to load that is supported by your language. Do not translate. * Set to 'off' to disable loading. */ $font_family = _x( 'Noto Serif:400,400i,700,700i', 'Google Font Name and Variants' ); if ( 'off' !== $font_family ) { $fonts_url = 'https://fonts.googleapis.com/css?family=' . urlencode( $font_family ); } $styles->add( 'wp-editor-font', $fonts_url ); // No longer used in core as of 5.7. $block_library_theme_path = WPINC . "/css/dist/block-library/theme$suffix.css"; $styles->add( 'wp-block-library-theme', "/$block_library_theme_path" ); $styles->add_data( 'wp-block-library-theme', 'path', ABSPATH . $block_library_theme_path ); $classic_theme_styles_path = WPINC . "/css/classic-themes$suffix.css"; $styles->add( 'classic-theme-styles', "/$classic_theme_styles_path" ); $styles->add_data( 'classic-theme-styles', 'path', ABSPATH . $classic_theme_styles_path ); $styles->add( 'wp-reset-editor-styles', "/wp-includes/css/dist/block-library/reset$suffix.css", array( 'common', 'forms' ) // Make sure the reset is loaded after the default WP Admin styles. ); $styles->add( 'wp-editor-classic-layout-styles', "/wp-includes/css/dist/edit-post/classic$suffix.css", array() ); $styles->add( 'wp-block-editor-content', "/wp-includes/css/dist/block-editor/content$suffix.css", array( 'wp-components' ) ); // Only add CONTENT styles here that should be enqueued in the iframe! $wp_edit_blocks_dependencies = array( 'wp-components', /* * This needs to be added before the block library styles, * The block library styles override the "reset" styles. */ 'wp-reset-editor-styles', 'wp-block-library', 'wp-block-editor-content', ); // Only load the default layout and margin styles for themes without theme.json file. if ( ! wp_theme_has_theme_json() ) { $wp_edit_blocks_dependencies[] = 'wp-editor-classic-layout-styles'; } if ( current_theme_supports( 'wp-block-styles' ) && ( ! is_array( $editor_styles ) || count( $editor_styles ) === 0 ) ) { /* * Include opinionated block styles if the theme supports block styles and * no $editor_styles are declared, so the editor never appears broken. */ $wp_edit_blocks_dependencies[] = 'wp-block-library-theme'; } $styles->add( 'wp-edit-blocks', "/wp-includes/css/dist/block-library/editor$suffix.css", $wp_edit_blocks_dependencies ); $package_styles = array( 'block-editor' => array( 'wp-components', 'wp-preferences' ), 'block-library' => array(), 'block-directory' => array(), 'components' => array(), 'commands' => array(), 'edit-post' => array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-commands', 'wp-preferences', ), 'editor' => array( 'wp-components', 'wp-block-editor', 'wp-reusable-blocks', 'wp-patterns', 'wp-preferences', ), 'format-library' => array(), 'list-reusable-blocks' => array( 'wp-components' ), 'reusable-blocks' => array( 'wp-components' ), 'patterns' => array( 'wp-components' ), 'preferences' => array( 'wp-components' ), 'nux' => array( 'wp-components' ), 'widgets' => array( 'wp-components', ), 'edit-widgets' => array( 'wp-widgets', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-patterns', 'wp-preferences', ), 'customize-widgets' => array( 'wp-widgets', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-patterns', 'wp-preferences', ), 'edit-site' => array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-commands', 'wp-preferences', ), ); foreach ( $package_styles as $package => $dependencies ) { $handle = 'wp-' . $package; $path = "/wp-includes/css/dist/$package/style$suffix.css"; if ( 'block-library' === $package && wp_should_load_separate_core_block_assets() ) { $path = "/wp-includes/css/dist/$package/common$suffix.css"; } $styles->add( $handle, $path, $dependencies ); $styles->add_data( $handle, 'path', ABSPATH . $path ); } // RTL CSS. $rtl_styles = array( // Admin CSS. 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n', 'install', 'wp-color-picker', 'customize-controls', 'customize-widgets', 'customize-nav-menus', 'customize-preview', 'login', 'site-health', 'wp-empty-template-alert', // Includes CSS. 'buttons', 'admin-bar', 'wp-auth-check', 'editor-buttons', 'media-views', 'wp-pointer', 'wp-jquery-ui-dialog', // Package styles. 'wp-reset-editor-styles', 'wp-editor-classic-layout-styles', 'wp-block-library-theme', 'wp-edit-blocks', 'wp-block-editor', 'wp-block-library', 'wp-block-directory', 'wp-commands', 'wp-components', 'wp-customize-widgets', 'wp-edit-post', 'wp-edit-site', 'wp-edit-widgets', 'wp-editor', 'wp-format-library', 'wp-list-reusable-blocks', 'wp-reusable-blocks', 'wp-patterns', 'wp-nux', 'wp-widgets', // Deprecated CSS. 'deprecated-media', 'farbtastic', ); foreach ( $rtl_styles as $rtl_style ) { $styles->add_data( $rtl_style, 'rtl', 'replace' ); if ( $suffix ) { $styles->add_data( $rtl_style, 'suffix', $suffix ); } } } /** * Reorders JavaScript scripts array to place prototype before jQuery. * * @since 2.3.1 * * @param string[] $js_array JavaScript scripts array * @return string[] Reordered array, if needed. */ function wp_prototype_before_jquery( $js_array ) { $prototype = array_search( 'prototype', $js_array, true ); if ( false === $prototype ) { return $js_array; } $jquery = array_search( 'jquery', $js_array, true ); if ( false === $jquery ) { return $js_array; } if ( $prototype < $jquery ) { return $js_array; } unset( $js_array[ $prototype ] ); array_splice( $js_array, $jquery, 0, 'prototype' ); return $js_array; } /** * Loads localized data on print rather than initialization. * * These localizations require information that may not be loaded even by init. * * @since 2.5.0 * * @global array $shortcode_tags */ function wp_just_in_time_script_localization() { wp_localize_script( 'autosave', 'autosaveL10n', array( 'autosaveInterval' => AUTOSAVE_INTERVAL, 'blog_id' => get_current_blog_id(), ) ); wp_localize_script( 'mce-view', 'mceViewL10n', array( 'shortcodes' => ! empty( $GLOBALS['shortcode_tags'] ) ? array_keys( $GLOBALS['shortcode_tags'] ) : array(), ) ); wp_localize_script( 'word-count', 'wordCountL10n', array( 'type' => wp_get_word_count_type(), 'shortcodes' => ! empty( $GLOBALS['shortcode_tags'] ) ? array_keys( $GLOBALS['shortcode_tags'] ) : array(), ) ); } /** * Localizes the jQuery UI datepicker. * * @since 4.6.0 * * @link https://api.jqueryui.com/datepicker/#options * * @global WP_Locale $wp_locale WordPress date and time locale object. */ function wp_localize_jquery_ui_datepicker() { global $wp_locale; if ( ! wp_script_is( 'jquery-ui-datepicker', 'enqueued' ) ) { return; } // Convert the PHP date format into jQuery UI's format. $datepicker_date_format = str_replace( array( 'd', 'j', 'l', 'z', // Day. 'F', 'M', 'n', 'm', // Month. 'Y', 'y', // Year. ), array( 'dd', 'd', 'DD', 'o', 'MM', 'M', 'm', 'mm', 'yy', 'y', ), get_option( 'date_format' ) ); $datepicker_defaults = wp_json_encode( array( 'closeText' => __( 'Close' ), 'currentText' => __( 'Today' ), 'monthNames' => array_values( $wp_locale->month ), 'monthNamesShort' => array_values( $wp_locale->month_abbrev ), 'nextText' => __( 'Next' ), 'prevText' => __( 'Previous' ), 'dayNames' => array_values( $wp_locale->weekday ), 'dayNamesShort' => array_values( $wp_locale->weekday_abbrev ), 'dayNamesMin' => array_values( $wp_locale->weekday_initial ), 'dateFormat' => $datepicker_date_format, 'firstDay' => absint( get_option( 'start_of_week' ) ), 'isRTL' => $wp_locale->is_rtl(), ) ); wp_add_inline_script( 'jquery-ui-datepicker', "jQuery(function(jQuery){jQuery.datepicker.setDefaults({$datepicker_defaults});});" ); } /** * Localizes community events data that needs to be passed to dashboard.js. * * @since 4.8.0 */ function wp_localize_community_events() { if ( ! wp_script_is( 'dashboard' ) ) { return; } require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php'; $user_id = get_current_user_id(); $saved_location = get_user_option( 'community-events-location', $user_id ); $saved_ip_address = isset( $saved_location['ip'] ) ? $saved_location['ip'] : false; $current_ip_address = WP_Community_Events::get_unsafe_client_ip(); /* * If the user's location is based on their IP address, then update their * location when their IP address changes. This allows them to see events * in their current city when travelling. Otherwise, they would always be * shown events in the city where they were when they first loaded the * Dashboard, which could have been months or years ago. */ if ( $saved_ip_address && $current_ip_address && $current_ip_address !== $saved_ip_address ) { $saved_location['ip'] = $current_ip_address; update_user_meta( $user_id, 'community-events-location', $saved_location ); } $events_client = new WP_Community_Events( $user_id, $saved_location ); wp_localize_script( 'dashboard', 'communityEventsData', array( 'nonce' => wp_create_nonce( 'community_events' ), 'cache' => $events_client->get_cached_events(), 'time_format' => get_option( 'time_format' ), ) ); } /** * Administration Screen CSS for changing the styles. * * If installing the 'wp-admin/' directory will be replaced with './'. * * The $_wp_admin_css_colors global manages the Administration Screens CSS * stylesheet that is loaded. The option that is set is 'admin_color' and is the * color and key for the array. The value for the color key is an object with * a 'url' parameter that has the URL path to the CSS file. * * The query from $src parameter will be appended to the URL that is given from * the $_wp_admin_css_colors array value URL. * * @since 2.6.0 * * @global array $_wp_admin_css_colors * * @param string $src Source URL. * @param string $handle Either 'colors' or 'colors-rtl'. * @return string|false URL path to CSS stylesheet for Administration Screens. */ function wp_style_loader_src( $src, $handle ) { global $_wp_admin_css_colors; if ( wp_installing() ) { return preg_replace( '#^wp-admin/#', './', $src ); } if ( 'colors' === $handle ) { $color = get_user_option( 'admin_color' ); if ( empty( $color ) || ! isset( $_wp_admin_css_colors[ $color ] ) ) { $color = 'fresh'; } $color = $_wp_admin_css_colors[ $color ]; $url = $color->url; if ( ! $url ) { return false; } $parsed = parse_url( $src ); if ( isset( $parsed['query'] ) && $parsed['query'] ) { wp_parse_str( $parsed['query'], $qv ); $url = add_query_arg( $qv, $url ); } return $url; } return $src; } /** * Prints the script queue in the HTML head on admin pages. * * Postpones the scripts that were queued for the footer. * print_footer_scripts() is called in the footer to print these scripts. * * @since 2.8.0 * * @see wp_print_scripts() * * @global bool $concatenate_scripts * * @return string[] Handles of the scripts that were printed. */ function print_head_scripts() { global $concatenate_scripts; if ( ! did_action( 'wp_print_scripts' ) ) { /** This action is documented in wp-includes/functions.wp-scripts.php */ do_action( 'wp_print_scripts' ); } $wp_scripts = wp_scripts(); script_concat_settings(); $wp_scripts->do_concat = $concatenate_scripts; $wp_scripts->do_head_items(); /** * Filters whether to print the head scripts. * * @since 2.8.0 * * @param bool $print Whether to print the head scripts. Default true. */ if ( apply_filters( 'print_head_scripts', true ) ) { _print_scripts(); } $wp_scripts->reset(); return $wp_scripts->done; } /** * Prints the scripts that were queued for the footer or too late for the HTML head. * * @since 2.8.0 * * @global WP_Scripts $wp_scripts * @global bool $concatenate_scripts * * @return string[] Handles of the scripts that were printed. */ function print_footer_scripts() { global $wp_scripts, $concatenate_scripts; if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { return array(); // No need to run if not instantiated. } script_concat_settings(); $wp_scripts->do_concat = $concatenate_scripts; $wp_scripts->do_footer_items(); /** * Filters whether to print the footer scripts. * * @since 2.8.0 * * @param bool $print Whether to print the footer scripts. Default true. */ if ( apply_filters( 'print_footer_scripts', true ) ) { _print_scripts(); } $wp_scripts->reset(); return $wp_scripts->done; } /** * Prints scripts (internal use only) * * @ignore * * @global WP_Scripts $wp_scripts * @global bool $compress_scripts */ function _print_scripts() { global $wp_scripts, $compress_scripts; $zip = $compress_scripts ? 1 : 0; if ( $zip && defined( 'ENFORCE_GZIP' ) && ENFORCE_GZIP ) { $zip = 'gzip'; } $concat = trim( $wp_scripts->concat, ', ' ); $type_attr = current_theme_supports( 'html5', 'script' ) ? '' : " type='text/javascript'"; if ( $concat ) { if ( ! empty( $wp_scripts->print_code ) ) { echo "\n\n"; echo "/* print_code; echo "/* ]]> */\n"; echo "\n"; } $concat = str_split( $concat, 128 ); $concatenated = ''; foreach ( $concat as $key => $chunk ) { $concatenated .= "&load%5Bchunk_{$key}%5D={$chunk}"; } $src = $wp_scripts->base_url . "/wp-admin/load-scripts.php?c={$zip}" . $concatenated . '&ver=' . $wp_scripts->default_version; echo "\n"; } if ( ! empty( $wp_scripts->print_html ) ) { echo $wp_scripts->print_html; } } /** * Prints the script queue in the HTML head on the front end. * * Postpones the scripts that were queued for the footer. * wp_print_footer_scripts() is called in the footer to print these scripts. * * @since 2.8.0 * * @global WP_Scripts $wp_scripts * * @return string[] Handles of the scripts that were printed. */ function wp_print_head_scripts() { global $wp_scripts; if ( ! did_action( 'wp_print_scripts' ) ) { /** This action is documented in wp-includes/functions.wp-scripts.php */ do_action( 'wp_print_scripts' ); } if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { return array(); // No need to run if nothing is queued. } return print_head_scripts(); } /** * Private, for use in *_footer_scripts hooks * * @since 3.3.0 */ function _wp_footer_scripts() { print_late_styles(); print_footer_scripts(); } /** * Hooks to print the scripts and styles in the footer. * * @since 2.8.0 */ function wp_print_footer_scripts() { /** * Fires when footer scripts are printed. * * @since 2.8.0 */ do_action( 'wp_print_footer_scripts' ); } /** * Wrapper for do_action( 'wp_enqueue_scripts' ). * * Allows plugins to queue scripts for the front end using wp_enqueue_script(). * Runs first in wp_head() where all is_home(), is_page(), etc. functions are available. * * @since 2.8.0 */ function wp_enqueue_scripts() { /** * Fires when scripts and styles are enqueued. * * @since 2.8.0 */ do_action( 'wp_enqueue_scripts' ); } /** * Prints the styles queue in the HTML head on admin pages. * * @since 2.8.0 * * @global bool $concatenate_scripts * * @return string[] Handles of the styles that were printed. */ function print_admin_styles() { global $concatenate_scripts; $wp_styles = wp_styles(); script_concat_settings(); $wp_styles->do_concat = $concatenate_scripts; $wp_styles->do_items( false ); /** * Filters whether to print the admin styles. * * @since 2.8.0 * * @param bool $print Whether to print the admin styles. Default true. */ if ( apply_filters( 'print_admin_styles', true ) ) { _print_styles(); } $wp_styles->reset(); return $wp_styles->done; } /** * Prints the styles that were queued too late for the HTML head. * * @since 3.3.0 * * @global WP_Styles $wp_styles * @global bool $concatenate_scripts * * @return array|void */ function print_late_styles() { global $wp_styles, $concatenate_scripts; if ( ! ( $wp_styles instanceof WP_Styles ) ) { return; } script_concat_settings(); $wp_styles->do_concat = $concatenate_scripts; $wp_styles->do_footer_items(); /** * Filters whether to print the styles queued too late for the HTML head. * * @since 3.3.0 * * @param bool $print Whether to print the 'late' styles. Default true. */ if ( apply_filters( 'print_late_styles', true ) ) { _print_styles(); } $wp_styles->reset(); return $wp_styles->done; } /** * Prints styles (internal use only). * * @ignore * @since 3.3.0 * * @global bool $compress_css */ function _print_styles() { global $compress_css; $wp_styles = wp_styles(); $zip = $compress_css ? 1 : 0; if ( $zip && defined( 'ENFORCE_GZIP' ) && ENFORCE_GZIP ) { $zip = 'gzip'; } $concat = trim( $wp_styles->concat, ', ' ); $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"'; if ( $concat ) { $dir = $wp_styles->text_direction; $ver = $wp_styles->default_version; $concat = str_split( $concat, 128 ); $concatenated = ''; foreach ( $concat as $key => $chunk ) { $concatenated .= "&load%5Bchunk_{$key}%5D={$chunk}"; } $href = $wp_styles->base_url . "/wp-admin/load-styles.php?c={$zip}&dir={$dir}" . $concatenated . '&ver=' . $ver; echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { echo "\n"; echo $wp_styles->print_code; echo "\n\n"; } } if ( ! empty( $wp_styles->print_html ) ) { echo $wp_styles->print_html; } } /** * Determines the concatenation and compression settings for scripts and styles. * * @since 2.8.0 * * @global bool $concatenate_scripts * @global bool $compress_scripts * @global bool $compress_css */ function script_concat_settings() { global $concatenate_scripts, $compress_scripts, $compress_css; $compressed_output = ( ini_get( 'zlib.output_compression' ) || 'ob_gzhandler' === ini_get( 'output_handler' ) ); $can_compress_scripts = ! wp_installing() && get_site_option( 'can_compress_scripts' ); if ( ! isset( $concatenate_scripts ) ) { $concatenate_scripts = defined( 'CONCATENATE_SCRIPTS' ) ? CONCATENATE_SCRIPTS : true; if ( ( ! is_admin() && ! did_action( 'login_init' ) ) || ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ) { $concatenate_scripts = false; } } if ( ! isset( $compress_scripts ) ) { $compress_scripts = defined( 'COMPRESS_SCRIPTS' ) ? COMPRESS_SCRIPTS : true; if ( $compress_scripts && ( ! $can_compress_scripts || $compressed_output ) ) { $compress_scripts = false; } } if ( ! isset( $compress_css ) ) { $compress_css = defined( 'COMPRESS_CSS' ) ? COMPRESS_CSS : true; if ( $compress_css && ( ! $can_compress_scripts || $compressed_output ) ) { $compress_css = false; } } } /** * Handles the enqueueing of block scripts and styles that are common to both * the editor and the front-end. * * @since 5.0.0 */ function wp_common_block_scripts_and_styles() { if ( is_admin() && ! wp_should_load_block_editor_scripts_and_styles() ) { return; } wp_enqueue_style( 'wp-block-library' ); if ( current_theme_supports( 'wp-block-styles' ) && ! wp_should_load_separate_core_block_assets() ) { wp_enqueue_style( 'wp-block-library-theme' ); } /** * Fires after enqueuing block assets for both editor and front-end. * * Call `add_action` on any hook before 'wp_enqueue_scripts'. * * In the function call you supply, simply use `wp_enqueue_script` and * `wp_enqueue_style` to add your functionality to the Gutenberg editor. * * @since 5.0.0 */ do_action( 'enqueue_block_assets' ); } /** * Applies a filter to the list of style nodes that comes from WP_Theme_JSON::get_style_nodes(). * * This particular filter removes all of the blocks from the array. * * We want WP_Theme_JSON to be ignorant of the implementation details of how the CSS is being used. * This filter allows us to modify the output of WP_Theme_JSON depending on whether or not we are * loading separate assets, without making the class aware of that detail. * * @since 6.1.0 * * @param array $nodes The nodes to filter. * @return array A filtered array of style nodes. */ function wp_filter_out_block_nodes( $nodes ) { return array_filter( $nodes, static function ( $node ) { return ! in_array( 'blocks', $node['path'], true ); }, ARRAY_FILTER_USE_BOTH ); } /** * Enqueues the global styles defined via theme.json. * * @since 5.8.0 */ function wp_enqueue_global_styles() { $assets_on_demand = wp_should_load_block_assets_on_demand(); $is_block_theme = wp_is_block_theme(); $is_classic_theme = ! $is_block_theme; /* * Global styles should be printed in the head for block themes, or for classic themes when loading assets on * demand is disabled, which is the default. * The footer should only be used for classic themes when loading assets on demand is enabled. * * See https://core.trac.wordpress.org/ticket/53494 and https://core.trac.wordpress.org/ticket/61965. */ if ( ( $is_block_theme && doing_action( 'wp_footer' ) ) || ( $is_classic_theme && doing_action( 'wp_footer' ) && ! $assets_on_demand ) || ( $is_classic_theme && doing_action( 'wp_enqueue_scripts' ) && $assets_on_demand ) ) { return; } /* * If loading the CSS for each block separately, then load the theme.json CSS conditionally. * This removes the CSS from the global-styles stylesheet and adds it to the inline CSS for each block. * This filter must be registered before calling wp_get_global_stylesheet(); */ add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); $stylesheet = wp_get_global_stylesheet(); if ( $is_block_theme ) { /* * Dequeue the Customizer's custom CSS * and add it before the global styles custom CSS. */ remove_action( 'wp_head', 'wp_custom_css_cb', 101 ); // Get the custom CSS from the Customizer and add it to the global stylesheet. $custom_css = wp_get_custom_css(); $stylesheet .= $custom_css; // Add the global styles custom CSS at the end. $stylesheet .= wp_get_global_stylesheet( array( 'custom-css' ) ); } if ( empty( $stylesheet ) ) { return; } wp_register_style( 'global-styles', false ); wp_add_inline_style( 'global-styles', $stylesheet ); wp_enqueue_style( 'global-styles' ); // Add each block as an inline css. wp_add_global_styles_for_blocks(); } /** * Checks if the editor scripts and styles for all registered block types * should be enqueued on the current screen. * * @since 5.6.0 * * @global WP_Screen $current_screen WordPress current screen object. * * @return bool Whether scripts and styles should be enqueued. */ function wp_should_load_block_editor_scripts_and_styles() { global $current_screen; $is_block_editor_screen = ( $current_screen instanceof WP_Screen ) && $current_screen->is_block_editor(); /** * Filters the flag that decides whether or not block editor scripts and styles * are going to be enqueued on the current screen. * * @since 5.6.0 * * @param bool $is_block_editor_screen Current value of the flag. */ return apply_filters( 'should_load_block_editor_scripts_and_styles', $is_block_editor_screen ); } /** * Checks whether separate styles should be loaded for core blocks. * * When this function returns true, other functions ensure that core blocks use their own separate stylesheets. * When this function returns false, all core blocks will use the single combined 'wp-block-library' stylesheet. * * As a side effect, the return value will by default result in block assets to be loaded on demand, via the * {@see wp_should_load_block_assets_on_demand()} function. This behavior can be separately altered via that function. * * This only affects front end and not the block editor screens. * * @since 5.8.0 * @see @see wp_should_load_block_assets_on_demand() * @see wp_enqueue_registered_block_scripts_and_styles() * @see register_block_style_handle() * * @return bool Whether separate core block assets will be loaded. */ function wp_should_load_separate_core_block_assets() { if ( is_admin() || is_feed() || wp_is_rest_endpoint() ) { return false; } /** * Filters whether block styles should be loaded separately. * * Returning false loads all core block assets, regardless of whether they are rendered * in a page or not. Returning true loads core block assets only when they are rendered. * * @since 5.8.0 * * @param bool $load_separate_assets Whether separate assets will be loaded. * Default false (all block assets are loaded, even when not used). */ return apply_filters( 'should_load_separate_core_block_assets', false ); } /** * Checks whether block styles should be loaded only on-render. * * When this function returns true, other functions ensure that blocks only load their assets on-render. * When this function returns false, all block assets are loaded regardless of whether they are rendered in a page. * * The default return value depends on the result of {@see wp_should_load_separate_core_block_assets()}, which controls * whether Core block stylesheets should be loaded separately or via a combined 'wp-block-library' stylesheet. * * This only affects front end and not the block editor screens. * * @since 6.8.0 * @see wp_should_load_separate_core_block_assets() * * @return bool Whether to load block assets only when they are rendered. */ function wp_should_load_block_assets_on_demand() { if ( is_admin() || is_feed() || wp_is_rest_endpoint() ) { return false; } /* * For backward compatibility, the default return value for this function is based on the return value of * `wp_should_load_separate_core_block_assets()`. Initially, this function used to control both of these concerns. */ $load_assets_on_demand = wp_should_load_separate_core_block_assets(); /** * Filters whether block styles should be loaded on demand. * * Returning false loads all block assets, regardless of whether they are rendered in a page or not. * Returning true loads block assets only when they are rendered. * * The default value of the filter depends on the result of {@see wp_should_load_separate_core_block_assets()}, * which controls whether Core block stylesheets should be loaded separately or via a combined 'wp-block-library' * stylesheet. * * @since 6.8.0 * * @param bool $load_assets_on_demand Whether to load block assets only when they are rendered. */ return apply_filters( 'should_load_block_assets_on_demand', $load_assets_on_demand ); } /** * Enqueues registered block scripts and styles, depending on current rendered * context (only enqueuing editor scripts while in context of the editor). * * @since 5.0.0 * * @global WP_Screen $current_screen WordPress current screen object. */ function wp_enqueue_registered_block_scripts_and_styles() { global $current_screen; if ( wp_should_load_block_assets_on_demand() ) { return; } $load_editor_scripts_and_styles = is_admin() && wp_should_load_block_editor_scripts_and_styles(); $block_registry = WP_Block_Type_Registry::get_instance(); /* * Block styles are only enqueued if they're registered. For core blocks, this is only the case if * `wp_should_load_separate_core_block_assets()` returns true. Otherwise they use the single combined * 'wp-block-library` stylesheet. See also `register_core_block_style_handles()`. * Since `wp_enqueue_style()` does not trigger warnings if the style is not registered, it is okay to not cater for * this behavior here and simply call `wp_enqueue_style()` unconditionally. */ foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) { // Front-end and editor styles. foreach ( $block_type->style_handles as $style_handle ) { wp_enqueue_style( $style_handle ); } // Front-end and editor scripts. foreach ( $block_type->script_handles as $script_handle ) { wp_enqueue_script( $script_handle ); } if ( $load_editor_scripts_and_styles ) { // Editor styles. foreach ( $block_type->editor_style_handles as $editor_style_handle ) { wp_enqueue_style( $editor_style_handle ); } // Editor scripts. foreach ( $block_type->editor_script_handles as $editor_script_handle ) { wp_enqueue_script( $editor_script_handle ); } } } } /** * Function responsible for enqueuing the styles required for block styles functionality on the editor and on the frontend. * * @since 5.3.0 * * @global WP_Styles $wp_styles */ function enqueue_block_styles_assets() { global $wp_styles; $block_styles = WP_Block_Styles_Registry::get_instance()->get_all_registered(); foreach ( $block_styles as $block_name => $styles ) { foreach ( $styles as $style_properties ) { if ( isset( $style_properties['style_handle'] ) ) { // If the site loads block styles on demand, enqueue the stylesheet on render. if ( wp_should_load_block_assets_on_demand() ) { add_filter( 'render_block', static function ( $html, $block ) use ( $block_name, $style_properties ) { if ( $block['blockName'] === $block_name ) { wp_enqueue_style( $style_properties['style_handle'] ); } return $html; }, 10, 2 ); } else { wp_enqueue_style( $style_properties['style_handle'] ); } } if ( isset( $style_properties['inline_style'] ) ) { // Default to "wp-block-library". $handle = 'wp-block-library'; // If the site loads block styles on demand, check if the block has a stylesheet registered. if ( wp_should_load_block_assets_on_demand() ) { $block_stylesheet_handle = generate_block_asset_handle( $block_name, 'style' ); if ( isset( $wp_styles->registered[ $block_stylesheet_handle ] ) ) { $handle = $block_stylesheet_handle; } } // Add inline styles to the calculated handle. wp_add_inline_style( $handle, $style_properties['inline_style'] ); } } } } /** * Function responsible for enqueuing the assets required for block styles functionality on the editor. * * @since 5.3.0 */ function enqueue_editor_block_styles_assets() { $block_styles = WP_Block_Styles_Registry::get_instance()->get_all_registered(); $register_script_lines = array( '( function() {' ); foreach ( $block_styles as $block_name => $styles ) { foreach ( $styles as $style_properties ) { $block_style = array( 'name' => $style_properties['name'], 'label' => $style_properties['label'], ); if ( isset( $style_properties['is_default'] ) ) { $block_style['isDefault'] = $style_properties['is_default']; } $register_script_lines[] = sprintf( ' wp.blocks.registerBlockStyle( \'%s\', %s );', $block_name, wp_json_encode( $block_style ) ); } } $register_script_lines[] = '} )();'; $inline_script = implode( "\n", $register_script_lines ); wp_register_script( 'wp-block-styles', false, array( 'wp-blocks' ), true, array( 'in_footer' => true ) ); wp_add_inline_script( 'wp-block-styles', $inline_script ); wp_enqueue_script( 'wp-block-styles' ); } /** * Enqueues the assets required for the block directory within the block editor. * * @since 5.5.0 */ function wp_enqueue_editor_block_directory_assets() { wp_enqueue_script( 'wp-block-directory' ); wp_enqueue_style( 'wp-block-directory' ); } /** * Enqueues the assets required for the format library within the block editor. * * @since 5.8.0 */ function wp_enqueue_editor_format_library_assets() { wp_enqueue_script( 'wp-format-library' ); wp_enqueue_style( 'wp-format-library' ); } /** * Sanitizes an attributes array into an attributes string to be placed inside a `\n", wp_sanitize_script_attributes( $attributes ) ); } /** * Prints formatted ` * * In an HTML document this would print "…" to the console, * but in an XHTML document it would print "…" to the console. * * * * In an HTML document this would print "An image is in HTML", * but it's an invalid XHTML document because it interprets the `` * as an empty tag missing its closing `/`. * * @see https://www.w3.org/TR/xhtml1/#h-4.8 */ if ( ! $is_html5 && ( ! isset( $attributes['type'] ) || 'module' === $attributes['type'] || str_contains( $attributes['type'], 'javascript' ) || str_contains( $attributes['type'], 'ecmascript' ) || str_contains( $attributes['type'], 'jscript' ) || str_contains( $attributes['type'], 'livescript' ) ) ) { /* * If the string `]]>` exists within the JavaScript it would break * out of any wrapping CDATA section added here, so to start, it's * necessary to escape that sequence which requires splitting the * content into two CDATA sections wherever it's found. * * Note: it's only necessary to escape the closing `]]>` because * an additional `', ']]]]>', $data ); // Wrap the entire escaped script inside a CDATA section. $data = sprintf( "/* */", $data ); } $data = "\n" . trim( $data, "\n\r " ) . "\n"; /** * Filters attributes to be added to a script tag. * * @since 5.7.0 * * @param array $attributes Key-value pairs representing `\n", wp_sanitize_script_attributes( $attributes ), $data ); } /** * Prints an inline script tag. * * It is possible to inject attributes in the `" from * around an inline script after trimming whitespace. Typically this * is used in conjunction with output buffering, where `ob_get_clean()` * is passed as the `$contents` argument. * * Example: * * // Strips exact literal empty SCRIPT tags. * $js = '; * 'sayHello();' === wp_remove_surrounding_empty_script_tags( $js ); * * // Otherwise if anything is different it warns in the JS console. * $js = ''; * 'console.error( ... )' === wp_remove_surrounding_empty_script_tags( $js ); * * @since 6.4.0 * @access private * * @see wp_print_inline_script_tag() * @see wp_get_inline_script_tag() * * @param string $contents Script body with manually created SCRIPT tag literals. * @return string Script body without surrounding script tag literals, or * original contents if both exact literals aren't present. */ function wp_remove_surrounding_empty_script_tags( $contents ) { $contents = trim( $contents ); $opener = ''; if ( strlen( $contents ) > strlen( $opener ) + strlen( $closer ) && strtoupper( substr( $contents, 0, strlen( $opener ) ) ) === $opener && strtoupper( substr( $contents, -strlen( $closer ) ) ) === $closer ) { return substr( $contents, strlen( $opener ), -strlen( $closer ) ); } else { $error_message = __( 'Expected string to start with script tag (without attributes) and end with script tag, with optional whitespace.' ); _doing_it_wrong( __FUNCTION__, $error_message, '6.4' ); return sprintf( 'console.error(%s)', wp_json_encode( sprintf( /* translators: %s: wp_remove_surrounding_empty_script_tags() */ __( 'Function %s used incorrectly in PHP.' ), 'wp_remove_surrounding_empty_script_tags()' ) . ' ' . $error_message ) ); } } ); } /** * Parses next element in the 'in table text' insertion mode. * * This internal function performs the 'in table text' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-intabletext * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_table_text(): bool { $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT . ' state.' ); } /** * Parses next element in the 'in caption' insertion mode. * * This internal function performs the 'in caption' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-incaption * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_caption(): bool { $tag_name = $this->get_tag(); $op_sigil = $this->is_tag_closer() ? '-' : '+'; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > An end tag whose tag name is "caption" * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr" * > An end tag whose tag name is "table" * * These tag handling rules are identical except for the final instruction. * Handle them in a single block. */ case '-CAPTION': case '+CAPTION': case '+COL': case '+COLGROUP': case '+TBODY': case '+TD': case '+TFOOT': case '+TH': case '+THEAD': case '+TR': case '-TABLE': if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'CAPTION' ) ) { // Parse error: ignore the token. return $this->step(); } $this->generate_implied_end_tags(); if ( ! $this->state->stack_of_open_elements->current_node_is( 'CAPTION' ) ) { // @todo Indicate a parse error once it's possible. } $this->state->stack_of_open_elements->pop_until( 'CAPTION' ); $this->state->active_formatting_elements->clear_up_to_last_marker(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; // If this is not a CAPTION end tag, the token should be reprocessed. if ( '-CAPTION' === $op ) { return true; } return $this->step( self::REPROCESS_CURRENT_NODE ); /** * > An end tag whose tag name is one of: "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ case '-BODY': case '-COL': case '-COLGROUP': case '-HTML': case '-TBODY': case '-TD': case '-TFOOT': case '-TH': case '-THEAD': case '-TR': // Parse error: ignore the token. return $this->step(); } /** * > Anything else * > Process the token using the rules for the "in body" insertion mode. */ return $this->step_in_body(); } /** * Parses next element in the 'in column group' insertion mode. * * This internal function performs the 'in column group' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-incolgroup * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_column_group(): bool { $token_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$token_name}"; switch ( $op ) { /* * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), * > U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ case '#text': if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) { // Insert the character. $this->insert_html_element( $this->state->current_token ); return true; } goto in_column_group_anything_else; break; /* * > A comment token */ case '#comment': case '#funky-comment': case '#presumptuous-tag': $this->insert_html_element( $this->state->current_token ); return true; /* * > A DOCTYPE token */ case 'html': // @todo Indicate a parse error once it's possible. return $this->step(); /* * > A start tag whose tag name is "html" */ case '+HTML': return $this->step_in_body(); /* * > A start tag whose tag name is "col" */ case '+COL': $this->insert_html_element( $this->state->current_token ); $this->state->stack_of_open_elements->pop(); return true; /* * > An end tag whose tag name is "colgroup" */ case '-COLGROUP': if ( ! $this->state->stack_of_open_elements->current_node_is( 'COLGROUP' ) ) { // @todo Indicate a parse error once it's possible. return $this->step(); } $this->state->stack_of_open_elements->pop(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; return true; /* * > An end tag whose tag name is "col" */ case '-COL': // Parse error: ignore the token. return $this->step(); /* * > A start tag whose tag name is "template" * > An end tag whose tag name is "template" */ case '+TEMPLATE': case '-TEMPLATE': return $this->step_in_head(); } in_column_group_anything_else: /* * > Anything else */ if ( ! $this->state->stack_of_open_elements->current_node_is( 'COLGROUP' ) ) { // @todo Indicate a parse error once it's possible. return $this->step(); } $this->state->stack_of_open_elements->pop(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; return $this->step( self::REPROCESS_CURRENT_NODE ); } /** * Parses next element in the 'in table body' insertion mode. * * This internal function performs the 'in table body' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-intbody * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_table_body(): bool { $tag_name = $this->get_tag(); $op_sigil = $this->is_tag_closer() ? '-' : '+'; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > A start tag whose tag name is "tr" */ case '+TR': $this->state->stack_of_open_elements->clear_to_table_body_context(); $this->insert_html_element( $this->state->current_token ); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; return true; /* * > A start tag whose tag name is one of: "th", "td" */ case '+TH': case '+TD': // @todo Indicate a parse error once it's possible. $this->state->stack_of_open_elements->clear_to_table_body_context(); $this->insert_virtual_node( 'TR' ); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ case '-TBODY': case '-TFOOT': case '-THEAD': if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { // Parse error: ignore the token. return $this->step(); } $this->state->stack_of_open_elements->clear_to_table_body_context(); $this->state->stack_of_open_elements->pop(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; return true; /* * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead" * > An end tag whose tag name is "table" */ case '+CAPTION': case '+COL': case '+COLGROUP': case '+TBODY': case '+TFOOT': case '+THEAD': case '-TABLE': if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TBODY' ) && ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'THEAD' ) && ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TFOOT' ) ) { // Parse error: ignore the token. return $this->step(); } $this->state->stack_of_open_elements->clear_to_table_body_context(); $this->state->stack_of_open_elements->pop(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th", "tr" */ case '-BODY': case '-CAPTION': case '-COL': case '-COLGROUP': case '-HTML': case '-TD': case '-TH': case '-TR': // Parse error: ignore the token. return $this->step(); } /* * > Anything else * > Process the token using the rules for the "in table" insertion mode. */ return $this->step_in_table(); } /** * Parses next element in the 'in row' insertion mode. * * This internal function performs the 'in row' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-intr * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_row(): bool { $tag_name = $this->get_tag(); $op_sigil = $this->is_tag_closer() ? '-' : '+'; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > A start tag whose tag name is one of: "th", "td" */ case '+TH': case '+TD': $this->state->stack_of_open_elements->clear_to_table_row_context(); $this->insert_html_element( $this->state->current_token ); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CELL; $this->state->active_formatting_elements->insert_marker(); return true; /* * > An end tag whose tag name is "tr" */ case '-TR': if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) { // Parse error: ignore the token. return $this->step(); } $this->state->stack_of_open_elements->clear_to_table_row_context(); $this->state->stack_of_open_elements->pop(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; return true; /* * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr" * > An end tag whose tag name is "table" */ case '+CAPTION': case '+COL': case '+COLGROUP': case '+TBODY': case '+TFOOT': case '+THEAD': case '+TR': case '-TABLE': if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) { // Parse error: ignore the token. return $this->step(); } $this->state->stack_of_open_elements->clear_to_table_row_context(); $this->state->stack_of_open_elements->pop(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ case '-TBODY': case '-TFOOT': case '-THEAD': if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { // Parse error: ignore the token. return $this->step(); } if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) { // Ignore the token. return $this->step(); } $this->state->stack_of_open_elements->clear_to_table_row_context(); $this->state->stack_of_open_elements->pop(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th" */ case '-BODY': case '-CAPTION': case '-COL': case '-COLGROUP': case '-HTML': case '-TD': case '-TH': // Parse error: ignore the token. return $this->step(); } /* * > Anything else * > Process the token using the rules for the "in table" insertion mode. */ return $this->step_in_table(); } /** * Parses next element in the 'in cell' insertion mode. * * This internal function performs the 'in cell' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-intd * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_cell(): bool { $tag_name = $this->get_tag(); $op_sigil = $this->is_tag_closer() ? '-' : '+'; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > An end tag whose tag name is one of: "td", "th" */ case '-TD': case '-TH': if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { // Parse error: ignore the token. return $this->step(); } $this->generate_implied_end_tags(); /* * @todo This needs to check if the current node is an HTML element, meaning that * when SVG and MathML support is added, this needs to differentiate between an * HTML element of the given name, such as `
`, and a foreign element of * the same given name. */ if ( ! $this->state->stack_of_open_elements->current_node_is( $tag_name ) ) { // @todo Indicate a parse error once it's possible. } $this->state->stack_of_open_elements->pop_until( $tag_name ); $this->state->active_formatting_elements->clear_up_to_last_marker(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; return true; /* * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", * > "tfoot", "th", "thead", "tr" */ case '+CAPTION': case '+COL': case '+COLGROUP': case '+TBODY': case '+TD': case '+TFOOT': case '+TH': case '+THEAD': case '+TR': /* * > Assert: The stack of open elements has a td or th element in table scope. * * Nothing to do here, except to verify in tests that this never appears. */ $this->close_cell(); return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html" */ case '-BODY': case '-CAPTION': case '-COL': case '-COLGROUP': case '-HTML': // Parse error: ignore the token. return $this->step(); /* * > An end tag whose tag name is one of: "table", "tbody", "tfoot", "thead", "tr" */ case '-TABLE': case '-TBODY': case '-TFOOT': case '-THEAD': case '-TR': if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { // Parse error: ignore the token. return $this->step(); } $this->close_cell(); return $this->step( self::REPROCESS_CURRENT_NODE ); } /* * > Anything else * > Process the token using the rules for the "in body" insertion mode. */ return $this->step_in_body(); } /** * Parses next element in the 'in select' insertion mode. * * This internal function performs the 'in select' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_select(): bool { $token_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$token_name}"; switch ( $op ) { /* * > Any other character token */ case '#text': /* * > A character token that is U+0000 NULL * * If a text node only comprises null bytes then it should be * entirely ignored and should not return to calling code. */ if ( parent::TEXT_IS_NULL_SEQUENCE === $this->text_node_classification ) { // Parse error: ignore the token. return $this->step(); } $this->insert_html_element( $this->state->current_token ); return true; /* * > A comment token */ case '#comment': case '#funky-comment': case '#presumptuous-tag': $this->insert_html_element( $this->state->current_token ); return true; /* * > A DOCTYPE token */ case 'html': // Parse error: ignore the token. return $this->step(); /* * > A start tag whose tag name is "html" */ case '+HTML': return $this->step_in_body(); /* * > A start tag whose tag name is "option" */ case '+OPTION': if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { $this->state->stack_of_open_elements->pop(); } $this->insert_html_element( $this->state->current_token ); return true; /* * > A start tag whose tag name is "optgroup" * > A start tag whose tag name is "hr" * * These rules are identical except for the treatment of the self-closing flag and * the subsequent pop of the HR void element, all of which is handled elsewhere in the processor. */ case '+OPTGROUP': case '+HR': if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { $this->state->stack_of_open_elements->pop(); } if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) { $this->state->stack_of_open_elements->pop(); } $this->insert_html_element( $this->state->current_token ); return true; /* * > An end tag whose tag name is "optgroup" */ case '-OPTGROUP': $current_node = $this->state->stack_of_open_elements->current_node(); if ( $current_node && 'OPTION' === $current_node->node_name ) { foreach ( $this->state->stack_of_open_elements->walk_up( $current_node ) as $parent ) { break; } if ( $parent && 'OPTGROUP' === $parent->node_name ) { $this->state->stack_of_open_elements->pop(); } } if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) { $this->state->stack_of_open_elements->pop(); return true; } // Parse error: ignore the token. return $this->step(); /* * > An end tag whose tag name is "option" */ case '-OPTION': if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { $this->state->stack_of_open_elements->pop(); return true; } // Parse error: ignore the token. return $this->step(); /* * > An end tag whose tag name is "select" * > A start tag whose tag name is "select" * * > It just gets treated like an end tag. */ case '-SELECT': case '+SELECT': if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) { // Parse error: ignore the token. return $this->step(); } $this->state->stack_of_open_elements->pop_until( 'SELECT' ); $this->reset_insertion_mode_appropriately(); return true; /* * > A start tag whose tag name is one of: "input", "keygen", "textarea" * * All three of these tags are considered a parse error when found in this insertion mode. */ case '+INPUT': case '+KEYGEN': case '+TEXTAREA': if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) { // Ignore the token. return $this->step(); } $this->state->stack_of_open_elements->pop_until( 'SELECT' ); $this->reset_insertion_mode_appropriately(); return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > A start tag whose tag name is one of: "script", "template" * > An end tag whose tag name is "template" */ case '+SCRIPT': case '+TEMPLATE': case '-TEMPLATE': return $this->step_in_head(); } /* * > Anything else * > Parse error: ignore the token. */ return $this->step(); } /** * Parses next element in the 'in select in table' insertion mode. * * This internal function performs the 'in select in table' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-inselectintable * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_select_in_table(): bool { $token_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$token_name}"; switch ( $op ) { /* * > A start tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th" */ case '+CAPTION': case '+TABLE': case '+TBODY': case '+TFOOT': case '+THEAD': case '+TR': case '+TD': case '+TH': // @todo Indicate a parse error once it's possible. $this->state->stack_of_open_elements->pop_until( 'SELECT' ); $this->reset_insertion_mode_appropriately(); return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > An end tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th" */ case '-CAPTION': case '-TABLE': case '-TBODY': case '-TFOOT': case '-THEAD': case '-TR': case '-TD': case '-TH': // @todo Indicate a parse error once it's possible. if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $token_name ) ) { return $this->step(); } $this->state->stack_of_open_elements->pop_until( 'SELECT' ); $this->reset_insertion_mode_appropriately(); return $this->step( self::REPROCESS_CURRENT_NODE ); } /* * > Anything else */ return $this->step_in_select(); } /** * Parses next element in the 'in template' insertion mode. * * This internal function performs the 'in template' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-intemplate * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_template(): bool { $token_name = $this->get_token_name(); $token_type = $this->get_token_type(); $is_closer = $this->is_tag_closer(); $op_sigil = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : ''; $op = "{$op_sigil}{$token_name}"; switch ( $op ) { /* * > A character token * > A comment token * > A DOCTYPE token */ case '#text': case '#comment': case '#funky-comment': case '#presumptuous-tag': case 'html': return $this->step_in_body(); /* * > A start tag whose tag name is one of: "base", "basefont", "bgsound", "link", * > "meta", "noframes", "script", "style", "template", "title" * > An end tag whose tag name is "template" */ case '+BASE': case '+BASEFONT': case '+BGSOUND': case '+LINK': case '+META': case '+NOFRAMES': case '+SCRIPT': case '+STYLE': case '+TEMPLATE': case '+TITLE': case '-TEMPLATE': return $this->step_in_head(); /* * > A start tag whose tag name is one of: "caption", "colgroup", "tbody", "tfoot", "thead" */ case '+CAPTION': case '+COLGROUP': case '+TBODY': case '+TFOOT': case '+THEAD': array_pop( $this->state->stack_of_template_insertion_modes ); $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > A start tag whose tag name is "col" */ case '+COL': array_pop( $this->state->stack_of_template_insertion_modes ); $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP; $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP; return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > A start tag whose tag name is "tr" */ case '+TR': array_pop( $this->state->stack_of_template_insertion_modes ); $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; return $this->step( self::REPROCESS_CURRENT_NODE ); /* * > A start tag whose tag name is one of: "td", "th" */ case '+TD': case '+TH': array_pop( $this->state->stack_of_template_insertion_modes ); $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; return $this->step( self::REPROCESS_CURRENT_NODE ); } /* * > Any other start tag */ if ( ! $is_closer ) { array_pop( $this->state->stack_of_template_insertion_modes ); $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; return $this->step( self::REPROCESS_CURRENT_NODE ); } /* * > Any other end tag */ if ( $is_closer ) { // Parse error: ignore the token. return $this->step(); } /* * > An end-of-file token */ if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) { // Stop parsing. return false; } // @todo Indicate a parse error once it's possible. $this->state->stack_of_open_elements->pop_until( 'TEMPLATE' ); $this->state->active_formatting_elements->clear_up_to_last_marker(); array_pop( $this->state->stack_of_template_insertion_modes ); $this->reset_insertion_mode_appropriately(); return $this->step( self::REPROCESS_CURRENT_NODE ); } /** * Parses next element in the 'after body' insertion mode. * * This internal function performs the 'after body' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-afterbody * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_after_body(): bool { $tag_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), * > U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE * * > Process the token using the rules for the "in body" insertion mode. */ case '#text': if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) { return $this->step_in_body(); } goto after_body_anything_else; break; /* * > A comment token */ case '#comment': case '#funky-comment': case '#presumptuous-tag': $this->bail( 'Content outside of BODY is unsupported.' ); break; /* * > A DOCTYPE token */ case 'html': // Parse error: ignore the token. return $this->step(); /* * > A start tag whose tag name is "html" */ case '+HTML': return $this->step_in_body(); /* * > An end tag whose tag name is "html" * * > If the parser was created as part of the HTML fragment parsing algorithm, * > this is a parse error; ignore the token. (fragment case) * > * > Otherwise, switch the insertion mode to "after after body". */ case '-HTML': if ( isset( $this->context_node ) ) { return $this->step(); } $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY; /* * The HTML element is not removed from the stack of open elements. * Only internal state has changed, this does not qualify as a "step" * in terms of advancing through the document to another token. * Nothing has been pushed or popped. * Proceed to parse the next item. */ return $this->step(); } /* * > Parse error. Switch the insertion mode to "in body" and reprocess the token. */ after_body_anything_else: $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; return $this->step( self::REPROCESS_CURRENT_NODE ); } /** * Parses next element in the 'in frameset' insertion mode. * * This internal function performs the 'in frameset' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-inframeset * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_frameset(): bool { $tag_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), * > U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE * > * > Insert the character. * * This algorithm effectively strips non-whitespace characters from text and inserts * them under HTML. This is not supported at this time. */ case '#text': if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) { return $this->step_in_body(); } $this->bail( 'Non-whitespace characters cannot be handled in frameset.' ); break; /* * > A comment token */ case '#comment': case '#funky-comment': case '#presumptuous-tag': $this->insert_html_element( $this->state->current_token ); return true; /* * > A DOCTYPE token */ case 'html': // Parse error: ignore the token. return $this->step(); /* * > A start tag whose tag name is "html" */ case '+HTML': return $this->step_in_body(); /* * > A start tag whose tag name is "frameset" */ case '+FRAMESET': $this->insert_html_element( $this->state->current_token ); return true; /* * > An end tag whose tag name is "frameset" */ case '-FRAMESET': /* * > If the current node is the root html element, then this is a parse error; * > ignore the token. (fragment case) */ if ( $this->state->stack_of_open_elements->current_node_is( 'HTML' ) ) { return $this->step(); } /* * > Otherwise, pop the current node from the stack of open elements. */ $this->state->stack_of_open_elements->pop(); /* * > If the parser was not created as part of the HTML fragment parsing algorithm * > (fragment case), and the current node is no longer a frameset element, then * > switch the insertion mode to "after frameset". */ if ( ! isset( $this->context_node ) && ! $this->state->stack_of_open_elements->current_node_is( 'FRAMESET' ) ) { $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET; } return true; /* * > A start tag whose tag name is "frame" * * > Insert an HTML element for the token. Immediately pop the * > current node off the stack of open elements. * > * > Acknowledge the token's self-closing flag, if it is set. */ case '+FRAME': $this->insert_html_element( $this->state->current_token ); $this->state->stack_of_open_elements->pop(); return true; /* * > A start tag whose tag name is "noframes" */ case '+NOFRAMES': return $this->step_in_head(); } // Parse error: ignore the token. return $this->step(); } /** * Parses next element in the 'after frameset' insertion mode. * * This internal function performs the 'after frameset' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-afterframeset * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_after_frameset(): bool { $tag_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), * > U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE * > * > Insert the character. * * This algorithm effectively strips non-whitespace characters from text and inserts * them under HTML. This is not supported at this time. */ case '#text': if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) { return $this->step_in_body(); } $this->bail( 'Non-whitespace characters cannot be handled in after frameset' ); break; /* * > A comment token */ case '#comment': case '#funky-comment': case '#presumptuous-tag': $this->insert_html_element( $this->state->current_token ); return true; /* * > A DOCTYPE token */ case 'html': // Parse error: ignore the token. return $this->step(); /* * > A start tag whose tag name is "html" */ case '+HTML': return $this->step_in_body(); /* * > An end tag whose tag name is "html" */ case '-HTML': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET; /* * The HTML element is not removed from the stack of open elements. * Only internal state has changed, this does not qualify as a "step" * in terms of advancing through the document to another token. * Nothing has been pushed or popped. * Proceed to parse the next item. */ return $this->step(); /* * > A start tag whose tag name is "noframes" */ case '+NOFRAMES': return $this->step_in_head(); } // Parse error: ignore the token. return $this->step(); } /** * Parses next element in the 'after after body' insertion mode. * * This internal function performs the 'after after body' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#the-after-after-body-insertion-mode * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_after_after_body(): bool { $tag_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > A comment token */ case '#comment': case '#funky-comment': case '#presumptuous-tag': $this->bail( 'Content outside of HTML is unsupported.' ); break; /* * > A DOCTYPE token * > A start tag whose tag name is "html" * * > Process the token using the rules for the "in body" insertion mode. */ case 'html': case '+HTML': return $this->step_in_body(); /* * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), * > U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE * > * > Process the token using the rules for the "in body" insertion mode. */ case '#text': if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) { return $this->step_in_body(); } goto after_after_body_anything_else; break; } /* * > Parse error. Switch the insertion mode to "in body" and reprocess the token. */ after_after_body_anything_else: $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; return $this->step( self::REPROCESS_CURRENT_NODE ); } /** * Parses next element in the 'after after frameset' insertion mode. * * This internal function performs the 'after after frameset' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#the-after-after-frameset-insertion-mode * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_after_after_frameset(): bool { $tag_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$tag_name}"; switch ( $op ) { /* * > A comment token */ case '#comment': case '#funky-comment': case '#presumptuous-tag': $this->bail( 'Content outside of HTML is unsupported.' ); break; /* * > A DOCTYPE token * > A start tag whose tag name is "html" * * > Process the token using the rules for the "in body" insertion mode. */ case 'html': case '+HTML': return $this->step_in_body(); /* * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), * > U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE * > * > Process the token using the rules for the "in body" insertion mode. * * This algorithm effectively strips non-whitespace characters from text and inserts * them under HTML. This is not supported at this time. */ case '#text': if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) { return $this->step_in_body(); } $this->bail( 'Non-whitespace characters cannot be handled in after after frameset.' ); break; /* * > A start tag whose tag name is "noframes" */ case '+NOFRAMES': return $this->step_in_head(); } // Parse error: ignore the token. return $this->step(); } /** * Parses next element in the 'in foreign content' insertion mode. * * This internal function performs the 'in foreign content' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#parsing-main-inforeign * @see WP_HTML_Processor::step * * @return bool Whether an element was found. */ private function step_in_foreign_content(): bool { $tag_name = $this->get_token_name(); $token_type = $this->get_token_type(); $op_sigil = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : ''; $op = "{$op_sigil}{$tag_name}"; /* * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size" * * This section drawn out above the switch to more easily incorporate * the additional rules based on the presence of the attributes. */ if ( '+FONT' === $op && ( null !== $this->get_attribute( 'color' ) || null !== $this->get_attribute( 'face' ) || null !== $this->get_attribute( 'size' ) ) ) { $op = '+FONT with attributes'; } switch ( $op ) { case '#text': /* * > A character token that is U+0000 NULL * * This is handled by `get_modifiable_text()`. */ /* * Whitespace-only text does not affect the frameset-ok flag. * It is probably inter-element whitespace, but it may also * contain character references which decode only to whitespace. */ if ( parent::TEXT_IS_GENERIC === $this->text_node_classification ) { $this->state->frameset_ok = false; } $this->insert_foreign_element( $this->state->current_token, false ); return true; /* * CDATA sections are alternate wrappers for text content and therefore * ought to follow the same rules as text nodes. */ case '#cdata-section': /* * NULL bytes and whitespace do not change the frameset-ok flag. */ $current_token = $this->bookmarks[ $this->state->current_token->bookmark_name ]; $cdata_content_start = $current_token->start + 9; $cdata_content_length = $current_token->length - 12; if ( strspn( $this->html, "\0 \t\n\f\r", $cdata_content_start, $cdata_content_length ) !== $cdata_content_length ) { $this->state->frameset_ok = false; } $this->insert_foreign_element( $this->state->current_token, false ); return true; /* * > A comment token */ case '#comment': case '#funky-comment': case '#presumptuous-tag': $this->insert_foreign_element( $this->state->current_token, false ); return true; /* * > A DOCTYPE token */ case 'html': // Parse error: ignore the token. return $this->step(); /* * > A start tag whose tag name is "b", "big", "blockquote", "body", "br", "center", * > "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", "h3", "h4", "h5", * > "h6", "head", "hr", "i", "img", "li", "listing", "menu", "meta", "nobr", "ol", * > "p", "pre", "ruby", "s", "small", "span", "strong", "strike", "sub", "sup", * > "table", "tt", "u", "ul", "var" * * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size" * * > An end tag whose tag name is "br", "p" * * Closing BR tags are always reported by the Tag Processor as opening tags. */ case '+B': case '+BIG': case '+BLOCKQUOTE': case '+BODY': case '+BR': case '+CENTER': case '+CODE': case '+DD': case '+DIV': case '+DL': case '+DT': case '+EM': case '+EMBED': case '+H1': case '+H2': case '+H3': case '+H4': case '+H5': case '+H6': case '+HEAD': case '+HR': case '+I': case '+IMG': case '+LI': case '+LISTING': case '+MENU': case '+META': case '+NOBR': case '+OL': case '+P': case '+PRE': case '+RUBY': case '+S': case '+SMALL': case '+SPAN': case '+STRONG': case '+STRIKE': case '+SUB': case '+SUP': case '+TABLE': case '+TT': case '+U': case '+UL': case '+VAR': case '+FONT with attributes': case '-BR': case '-P': // @todo Indicate a parse error once it's possible. foreach ( $this->state->stack_of_open_elements->walk_up() as $current_node ) { if ( 'math' === $current_node->integration_node_type || 'html' === $current_node->integration_node_type || 'html' === $current_node->namespace ) { break; } $this->state->stack_of_open_elements->pop(); } goto in_foreign_content_process_in_current_insertion_mode; } /* * > Any other start tag */ if ( ! $this->is_tag_closer() ) { $this->insert_foreign_element( $this->state->current_token, false ); /* * > If the token has its self-closing flag set, then run * > the appropriate steps from the following list: * > * > ↪ the token's tag name is "script", and the new current node is in the SVG namespace * > Acknowledge the token's self-closing flag, and then act as * > described in the steps for a "script" end tag below. * > * > ↪ Otherwise * > Pop the current node off the stack of open elements and * > acknowledge the token's self-closing flag. * * Since the rules for SCRIPT below indicate to pop the element off of the stack of * open elements, which is the same for the Otherwise condition, there's no need to * separate these checks. The difference comes when a parser operates with the scripting * flag enabled, and executes the script, which this parser does not support. */ if ( $this->state->current_token->has_self_closing_flag ) { $this->state->stack_of_open_elements->pop(); } return true; } /* * > An end tag whose name is "script", if the current node is an SVG script element. */ if ( $this->is_tag_closer() && 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) { $this->state->stack_of_open_elements->pop(); return true; } /* * > Any other end tag */ if ( $this->is_tag_closer() ) { $node = $this->state->stack_of_open_elements->current_node(); if ( $tag_name !== $node->node_name ) { // @todo Indicate a parse error once it's possible. } in_foreign_content_end_tag_loop: if ( $node === $this->state->stack_of_open_elements->at( 1 ) ) { return true; } /* * > If node's tag name, converted to ASCII lowercase, is the same as the tag name * > of the token, pop elements from the stack of open elements until node has * > been popped from the stack, and then return. */ if ( 0 === strcasecmp( $node->node_name, $tag_name ) ) { foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) { $this->state->stack_of_open_elements->pop(); if ( $node === $item ) { return true; } } } foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) { $node = $item; break; } if ( 'html' !== $node->namespace ) { goto in_foreign_content_end_tag_loop; } in_foreign_content_process_in_current_insertion_mode: switch ( $this->state->insertion_mode ) { case WP_HTML_Processor_State::INSERTION_MODE_INITIAL: return $this->step_initial(); case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML: return $this->step_before_html(); case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD: return $this->step_before_head(); case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD: return $this->step_in_head(); case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT: return $this->step_in_head_noscript(); case WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD: return $this->step_after_head(); case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY: return $this->step_in_body(); case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE: return $this->step_in_table(); case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT: return $this->step_in_table_text(); case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION: return $this->step_in_caption(); case WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP: return $this->step_in_column_group(); case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY: return $this->step_in_table_body(); case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW: return $this->step_in_row(); case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL: return $this->step_in_cell(); case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT: return $this->step_in_select(); case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE: return $this->step_in_select_in_table(); case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE: return $this->step_in_template(); case WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY: return $this->step_after_body(); case WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET: return $this->step_in_frameset(); case WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET: return $this->step_after_frameset(); case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY: return $this->step_after_after_body(); case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET: return $this->step_after_after_frameset(); // This should be unreachable but PHP doesn't have total type checking on switch. default: $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." ); } } $this->bail( 'Should not have been able to reach end of IN FOREIGN CONTENT processing. Check HTML API code.' ); // This unnecessary return prevents tools from inaccurately reporting type errors. return false; } /* * Internal helpers */ /** * Creates a new bookmark for the currently-matched token and returns the generated name. * * @since 6.4.0 * @since 6.5.0 Renamed from bookmark_tag() to bookmark_token(). * * @throws Exception When unable to allocate requested bookmark. * * @return string|false Name of created bookmark, or false if unable to create. */ private function bookmark_token() { if ( ! parent::set_bookmark( ++$this->bookmark_counter ) ) { $this->last_error = self::ERROR_EXCEEDED_MAX_BOOKMARKS; throw new Exception( 'could not allocate bookmark' ); } return "{$this->bookmark_counter}"; } /* * HTML semantic overrides for Tag Processor */ /** * Indicates the namespace of the current token, or "html" if there is none. * * @return string One of "html", "math", or "svg". */ public function get_namespace(): string { if ( ! isset( $this->current_element ) ) { return parent::get_namespace(); } return $this->current_element->token->namespace; } /** * Returns the uppercase name of the matched tag. * * The semantic rules for HTML specify that certain tags be reprocessed * with a different tag name. Because of this, the tag name presented * by the HTML Processor may differ from the one reported by the HTML * Tag Processor, which doesn't apply these semantic rules. * * Example: * * $processor = new WP_HTML_Tag_Processor( '
Test
' ); * $processor->next_tag() === true; * $processor->get_tag() === 'DIV'; * * $processor->next_tag() === false; * $processor->get_tag() === null; * * @since 6.4.0 * * @return string|null Name of currently matched tag in input HTML, or `null` if none found. */ public function get_tag(): ?string { if ( null !== $this->last_error ) { return null; } if ( $this->is_virtual() ) { return $this->current_element->token->node_name; } $tag_name = parent::get_tag(); /* * > A start tag whose tag name is "image" * > Change the token's tag name to "img" and reprocess it. (Don't ask.) */ return ( 'IMAGE' === $tag_name && 'html' === $this->get_namespace() ) ? 'IMG' : $tag_name; } /** * Indicates if the currently matched tag contains the self-closing flag. * * No HTML elements ought to have the self-closing flag and for those, the self-closing * flag will be ignored. For void elements this is benign because they "self close" * automatically. For non-void HTML elements though problems will appear if someone * intends to use a self-closing element in place of that element with an empty body. * For HTML foreign elements and custom elements the self-closing flag determines if * they self-close or not. * * This function does not determine if a tag is self-closing, * but only if the self-closing flag is present in the syntax. * * @since 6.6.0 Subclassed for the HTML Processor. * * @return bool Whether the currently matched tag contains the self-closing flag. */ public function has_self_closing_flag(): bool { return $this->is_virtual() ? false : parent::has_self_closing_flag(); } /** * Returns the node name represented by the token. * * This matches the DOM API value `nodeName`. Some values * are static, such as `#text` for a text node, while others * are dynamically generated from the token itself. * * Dynamic names: * - Uppercase tag name for tag matches. * - `html` for DOCTYPE declarations. * * Note that if the Tag Processor is not matched on a token * then this function will return `null`, either because it * hasn't yet found a token or because it reached the end * of the document without matching a token. * * @since 6.6.0 Subclassed for the HTML Processor. * * @return string|null Name of the matched token. */ public function get_token_name(): ?string { return $this->is_virtual() ? $this->current_element->token->node_name : parent::get_token_name(); } /** * Indicates the kind of matched token, if any. * * This differs from `get_token_name()` in that it always * returns a static string indicating the type, whereas * `get_token_name()` may return values derived from the * token itself, such as a tag name or processing * instruction tag. * * Possible values: * - `#tag` when matched on a tag. * - `#text` when matched on a text node. * - `#cdata-section` when matched on a CDATA node. * - `#comment` when matched on a comment. * - `#doctype` when matched on a DOCTYPE declaration. * - `#presumptuous-tag` when matched on an empty tag closer. * - `#funky-comment` when matched on a funky comment. * * @since 6.6.0 Subclassed for the HTML Processor. * * @return string|null What kind of token is matched, or null. */ public function get_token_type(): ?string { if ( $this->is_virtual() ) { /* * This logic comes from the Tag Processor. * * @todo It would be ideal not to repeat this here, but it's not clearly * better to allow passing a token name to `get_token_type()`. */ $node_name = $this->current_element->token->node_name; $starting_char = $node_name[0]; if ( 'A' <= $starting_char && 'Z' >= $starting_char ) { return '#tag'; } if ( 'html' === $node_name ) { return '#doctype'; } return $node_name; } return parent::get_token_type(); } /** * Returns the value of a requested attribute from a matched tag opener if that attribute exists. * * Example: * * $p = WP_HTML_Processor::create_fragment( '
Test
' ); * $p->next_token() === true; * $p->get_attribute( 'data-test-id' ) === '14'; * $p->get_attribute( 'enabled' ) === true; * $p->get_attribute( 'aria-label' ) === null; * * $p->next_tag() === false; * $p->get_attribute( 'class' ) === null; * * @since 6.6.0 Subclassed for HTML Processor. * * @param string $name Name of attribute whose value is requested. * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`. */ public function get_attribute( $name ) { return $this->is_virtual() ? null : parent::get_attribute( $name ); } /** * Updates or creates a new attribute on the currently matched tag with the passed value. * * For boolean attributes special handling is provided: * - When `true` is passed as the value, then only the attribute name is added to the tag. * - When `false` is passed, the attribute gets removed if it existed before. * * For string attributes, the value is escaped using the `esc_attr` function. * * @since 6.6.0 Subclassed for the HTML Processor. * * @param string $name The attribute name to target. * @param string|bool $value The new attribute value. * @return bool Whether an attribute value was set. */ public function set_attribute( $name, $value ): bool { return $this->is_virtual() ? false : parent::set_attribute( $name, $value ); } /** * Remove an attribute from the currently-matched tag. * * @since 6.6.0 Subclassed for HTML Processor. * * @param string $name The attribute name to remove. * @return bool Whether an attribute was removed. */ public function remove_attribute( $name ): bool { return $this->is_virtual() ? false : parent::remove_attribute( $name ); } /** * Gets lowercase names of all attributes matching a given prefix in the current tag. * * Note that matching is case-insensitive. This is in accordance with the spec: * * > There must never be two or more attributes on * > the same start tag whose names are an ASCII * > case-insensitive match for each other. * - HTML 5 spec * * Example: * * $p = new WP_HTML_Tag_Processor( '
Test
' ); * $p->next_tag( array( 'class_name' => 'test' ) ) === true; * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); * * $p->next_tag() === false; * $p->get_attribute_names_with_prefix( 'data-' ) === null; * * @since 6.6.0 Subclassed for the HTML Processor. * * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive * * @param string $prefix Prefix of requested attribute names. * @return array|null List of attribute names, or `null` when no tag opener is matched. */ public function get_attribute_names_with_prefix( $prefix ): ?array { return $this->is_virtual() ? null : parent::get_attribute_names_with_prefix( $prefix ); } /** * Adds a new class name to the currently matched tag. * * @since 6.6.0 Subclassed for the HTML Processor. * * @param string $class_name The class name to add. * @return bool Whether the class was set to be added. */ public function add_class( $class_name ): bool { return $this->is_virtual() ? false : parent::add_class( $class_name ); } /** * Removes a class name from the currently matched tag. * * @since 6.6.0 Subclassed for the HTML Processor. * * @param string $class_name The class name to remove. * @return bool Whether the class was set to be removed. */ public function remove_class( $class_name ): bool { return $this->is_virtual() ? false : parent::remove_class( $class_name ); } /** * Returns if a matched tag contains the given ASCII case-insensitive class name. * * @since 6.6.0 Subclassed for the HTML Processor. * * @todo When reconstructing active formatting elements with attributes, find a way * to indicate if the virtually-reconstructed formatting elements contain the * wanted class name. * * @param string $wanted_class Look for this CSS class name, ASCII case-insensitive. * @return bool|null Whether the matched tag contains the given class name, or null if not matched. */ public function has_class( $wanted_class ): ?bool { return $this->is_virtual() ? null : parent::has_class( $wanted_class ); } /** * Generator for a foreach loop to step through each class name for the matched tag. * * This generator function is designed to be used inside a "foreach" loop. * * Example: * * $p = WP_HTML_Processor::create_fragment( "
" ); * $p->next_tag(); * foreach ( $p->class_list() as $class_name ) { * echo "{$class_name} "; * } * // Outputs: "free lang-en " * * @since 6.6.0 Subclassed for the HTML Processor. */ public function class_list() { return $this->is_virtual() ? null : parent::class_list(); } /** * Returns the modifiable text for a matched token, or an empty string. * * Modifiable text is text content that may be read and changed without * changing the HTML structure of the document around it. This includes * the contents of `#text` nodes in the HTML as well as the inner * contents of HTML comments, Processing Instructions, and others, even * though these nodes aren't part of a parsed DOM tree. They also contain * the contents of SCRIPT and STYLE tags, of TEXTAREA tags, and of any * other section in an HTML document which cannot contain HTML markup (DATA). * * If a token has no modifiable text then an empty string is returned to * avoid needless crashing or type errors. An empty string does not mean * that a token has modifiable text, and a token with modifiable text may * have an empty string (e.g. a comment with no contents). * * @since 6.6.0 Subclassed for the HTML Processor. * * @return string */ public function get_modifiable_text(): string { return $this->is_virtual() ? '' : parent::get_modifiable_text(); } /** * Indicates what kind of comment produced the comment node. * * Because there are different kinds of HTML syntax which produce * comments, the Tag Processor tracks and exposes this as a type * for the comment. Nominally only regular HTML comments exist as * they are commonly known, but a number of unrelated syntax errors * also produce comments. * * @see self::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT * @see self::COMMENT_AS_CDATA_LOOKALIKE * @see self::COMMENT_AS_INVALID_HTML * @see self::COMMENT_AS_HTML_COMMENT * @see self::COMMENT_AS_PI_NODE_LOOKALIKE * * @since 6.6.0 Subclassed for the HTML Processor. * * @return string|null */ public function get_comment_type(): ?string { return $this->is_virtual() ? null : parent::get_comment_type(); } /** * Removes a bookmark that is no longer needed. * * Releasing a bookmark frees up the small * performance overhead it requires. * * @since 6.4.0 * * @param string $bookmark_name Name of the bookmark to remove. * @return bool Whether the bookmark already existed before removal. */ public function release_bookmark( $bookmark_name ): bool { return parent::release_bookmark( "_{$bookmark_name}" ); } /** * Moves the internal cursor in the HTML Processor to a given bookmark's location. * * Be careful! Seeking backwards to a previous location resets the parser to the * start of the document and reparses the entire contents up until it finds the * sought-after bookmarked location. * * In order to prevent accidental infinite loops, there's a * maximum limit on the number of times seek() can be called. * * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document. * * @since 6.4.0 * * @param string $bookmark_name Jump to the place in the document identified by this bookmark name. * @return bool Whether the internal cursor was successfully moved to the bookmark's location. */ public function seek( $bookmark_name ): bool { // Flush any pending updates to the document before beginning. $this->get_updated_html(); $actual_bookmark_name = "_{$bookmark_name}"; $processor_started_at = $this->state->current_token ? $this->bookmarks[ $this->state->current_token->bookmark_name ]->start : 0; $bookmark_starts_at = $this->bookmarks[ $actual_bookmark_name ]->start; $direction = $bookmark_starts_at > $processor_started_at ? 'forward' : 'backward'; /* * If seeking backwards, it's possible that the sought-after bookmark exists within an element * which has been closed before the current cursor; in other words, it has already been removed * from the stack of open elements. This means that it's insufficient to simply pop off elements * from the stack of open elements which appear after the bookmarked location and then jump to * that location, as the elements which were open before won't be re-opened. * * In order to maintain consistency, the HTML Processor rewinds to the start of the document * and reparses everything until it finds the sought-after bookmark. * * There are potentially better ways to do this: cache the parser state for each bookmark and * restore it when seeking; store an immutable and idempotent register of where elements open * and close. * * If caching the parser state it will be essential to properly maintain the cached stack of * open elements and active formatting elements when modifying the document. This could be a * tedious and time-consuming process as well, and so for now will not be performed. * * It may be possible to track bookmarks for where elements open and close, and in doing so * be able to quickly recalculate breadcrumbs for any element in the document. It may even * be possible to remove the stack of open elements and compute it on the fly this way. * If doing this, the parser would need to track the opening and closing locations for all * tokens in the breadcrumb path for any and all bookmarks. By utilizing bookmarks themselves * this list could be automatically maintained while modifying the document. Finding the * breadcrumbs would then amount to traversing that list from the start until the token * being inspected. Once an element closes, if there are no bookmarks pointing to locations * within that element, then all of these locations may be forgotten to save on memory use * and computation time. */ if ( 'backward' === $direction ) { /* * When moving backward, stateful stacks should be cleared. */ foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) { $this->state->stack_of_open_elements->remove_node( $item ); } foreach ( $this->state->active_formatting_elements->walk_up() as $item ) { $this->state->active_formatting_elements->remove_node( $item ); } /* * **After** clearing stacks, more processor state can be reset. * This must be done after clearing the stack because those stacks generate events that * would appear on a subsequent call to `next_token()`. */ $this->state->frameset_ok = true; $this->state->stack_of_template_insertion_modes = array(); $this->state->head_element = null; $this->state->form_element = null; $this->state->current_token = null; $this->current_element = null; $this->element_queue = array(); /* * The absence of a context node indicates a full parse. * The presence of a context node indicates a fragment parser. */ if ( null === $this->context_node ) { $this->change_parsing_namespace( 'html' ); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_INITIAL; $this->breadcrumbs = array(); $this->bookmarks['initial'] = new WP_HTML_Span( 0, 0 ); parent::seek( 'initial' ); unset( $this->bookmarks['initial'] ); } else { /* * Push the root-node (HTML) back onto the stack of open elements. * * Fragment parsers require this extra bit of setup. * It's handled in full parsers by advancing the processor state. */ $this->state->stack_of_open_elements->push( new WP_HTML_Token( 'root-node', 'HTML', false ) ); $this->change_parsing_namespace( $this->context_node->integration_node_type ? 'html' : $this->context_node->namespace ); if ( 'TEMPLATE' === $this->context_node->node_name ) { $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE; } $this->reset_insertion_mode_appropriately(); $this->breadcrumbs = array_slice( $this->breadcrumbs, 0, 2 ); parent::seek( $this->context_node->bookmark_name ); } } /* * Here, the processor moves forward through the document until it matches the bookmark. * do-while is used here because the processor is expected to already be stopped on * a token than may match the bookmarked location. */ do { /* * The processor will stop on virtual tokens, but bookmarks may not be set on them. * They should not be matched when seeking a bookmark, skip them. */ if ( $this->is_virtual() ) { continue; } if ( $bookmark_starts_at === $this->bookmarks[ $this->state->current_token->bookmark_name ]->start ) { return true; } } while ( $this->next_token() ); return false; } /** * Sets a bookmark in the HTML document. * * Bookmarks represent specific places or tokens in the HTML * document, such as a tag opener or closer. When applying * edits to a document, such as setting an attribute, the * text offsets of that token may shift; the bookmark is * kept updated with those shifts and remains stable unless * the entire span of text in which the token sits is removed. * * Release bookmarks when they are no longer needed. * * Example: * *

Surprising fact you may not know!

* ^ ^ * \-|-- this `H2` opener bookmark tracks the token * *

Surprising fact you may no… * ^ ^ * \-|-- it shifts with edits * * Bookmarks provide the ability to seek to a previously-scanned * place in the HTML document. This avoids the need to re-scan * the entire document. * * Example: * *
  • One
  • Two
  • Three
* ^^^^ * want to note this last item * * $p = new WP_HTML_Tag_Processor( $html ); * $in_list = false; * while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) { * if ( 'UL' === $p->get_tag() ) { * if ( $p->is_tag_closer() ) { * $in_list = false; * $p->set_bookmark( 'resume' ); * if ( $p->seek( 'last-li' ) ) { * $p->add_class( 'last-li' ); * } * $p->seek( 'resume' ); * $p->release_bookmark( 'last-li' ); * $p->release_bookmark( 'resume' ); * } else { * $in_list = true; * } * } * * if ( 'LI' === $p->get_tag() ) { * $p->set_bookmark( 'last-li' ); * } * } * * Bookmarks intentionally hide the internal string offsets * to which they refer. They are maintained internally as * updates are applied to the HTML document and therefore * retain their "position" - the location to which they * originally pointed. The inability to use bookmarks with * functions like `substr` is therefore intentional to guard * against accidentally breaking the HTML. * * Because bookmarks allocate memory and require processing * for every applied update, they are limited and require * a name. They should not be created with programmatically-made * names, such as "li_{$index}" with some loop. As a general * rule they should only be created with string-literal names * like "start-of-section" or "last-paragraph". * * Bookmarks are a powerful tool to enable complicated behavior. * Consider double-checking that you need this tool if you are * reaching for it, as inappropriate use could lead to broken * HTML structure or unwanted processing overhead. * * Bookmarks cannot be set on tokens that do no appear in the original * HTML text. For example, the HTML `
` stops at tags `TABLE`, * `TBODY`, `TR`, and `TD`. The `TBODY` and `TR` tags do not appear in * the original HTML and cannot be used as bookmarks. * * @since 6.4.0 * * @param string $bookmark_name Identifies this particular bookmark. * @return bool Whether the bookmark was successfully created. */ public function set_bookmark( $bookmark_name ): bool { if ( $this->is_virtual() ) { _doing_it_wrong( __METHOD__, __( 'Cannot set bookmarks on tokens that do no appear in the original HTML text.' ), '6.8.0' ); return false; } return parent::set_bookmark( "_{$bookmark_name}" ); } /** * Checks whether a bookmark with the given name exists. * * @since 6.5.0 * * @param string $bookmark_name Name to identify a bookmark that potentially exists. * @return bool Whether that bookmark exists. */ public function has_bookmark( $bookmark_name ): bool { return parent::has_bookmark( "_{$bookmark_name}" ); } /* * HTML Parsing Algorithms */ /** * Closes a P element. * * @since 6.4.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#close-a-p-element */ private function close_a_p_element(): void { $this->generate_implied_end_tags( 'P' ); $this->state->stack_of_open_elements->pop_until( 'P' ); } /** * Closes elements that have implied end tags. * * @since 6.4.0 * @since 6.7.0 Full spec support. * * @see https://html.spec.whatwg.org/#generate-implied-end-tags * * @param string|null $except_for_this_element Perform as if this element doesn't exist in the stack of open elements. */ private function generate_implied_end_tags( ?string $except_for_this_element = null ): void { $elements_with_implied_end_tags = array( 'DD', 'DT', 'LI', 'OPTGROUP', 'OPTION', 'P', 'RB', 'RP', 'RT', 'RTC', ); $no_exclusions = ! isset( $except_for_this_element ); while ( ( $no_exclusions || ! $this->state->stack_of_open_elements->current_node_is( $except_for_this_element ) ) && in_array( $this->state->stack_of_open_elements->current_node()->node_name, $elements_with_implied_end_tags, true ) ) { $this->state->stack_of_open_elements->pop(); } } /** * Closes elements that have implied end tags, thoroughly. * * See the HTML specification for an explanation why this is * different from generating end tags in the normal sense. * * @since 6.4.0 * @since 6.7.0 Full spec support. * * @see WP_HTML_Processor::generate_implied_end_tags * @see https://html.spec.whatwg.org/#generate-implied-end-tags */ private function generate_implied_end_tags_thoroughly(): void { $elements_with_implied_end_tags = array( 'CAPTION', 'COLGROUP', 'DD', 'DT', 'LI', 'OPTGROUP', 'OPTION', 'P', 'RB', 'RP', 'RT', 'RTC', 'TBODY', 'TD', 'TFOOT', 'TH', 'THEAD', 'TR', ); while ( in_array( $this->state->stack_of_open_elements->current_node()->node_name, $elements_with_implied_end_tags, true ) ) { $this->state->stack_of_open_elements->pop(); } } /** * Returns the adjusted current node. * * > The adjusted current node is the context element if the parser was created as * > part of the HTML fragment parsing algorithm and the stack of open elements * > has only one element in it (fragment case); otherwise, the adjusted current * > node is the current node. * * @see https://html.spec.whatwg.org/#adjusted-current-node * * @since 6.7.0 * * @return WP_HTML_Token|null The adjusted current node. */ private function get_adjusted_current_node(): ?WP_HTML_Token { if ( isset( $this->context_node ) && 1 === $this->state->stack_of_open_elements->count() ) { return $this->context_node; } return $this->state->stack_of_open_elements->current_node(); } /** * Reconstructs the active formatting elements. * * > This has the effect of reopening all the formatting elements that were opened * > in the current body, cell, or caption (whichever is youngest) that haven't * > been explicitly closed. * * @since 6.4.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#reconstruct-the-active-formatting-elements * * @return bool Whether any formatting elements needed to be reconstructed. */ private function reconstruct_active_formatting_elements(): bool { /* * > If there are no entries in the list of active formatting elements, then there is nothing * > to reconstruct; stop this algorithm. */ if ( 0 === $this->state->active_formatting_elements->count() ) { return false; } $last_entry = $this->state->active_formatting_elements->current_node(); if ( /* * > If the last (most recently added) entry in the list of active formatting elements is a marker; * > stop this algorithm. */ 'marker' === $last_entry->node_name || /* * > If the last (most recently added) entry in the list of active formatting elements is an * > element that is in the stack of open elements, then there is nothing to reconstruct; * > stop this algorithm. */ $this->state->stack_of_open_elements->contains_node( $last_entry ) ) { return false; } $this->bail( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' ); } /** * Runs the reset the insertion mode appropriately algorithm. * * @since 6.7.0 * * @see https://html.spec.whatwg.org/multipage/parsing.html#reset-the-insertion-mode-appropriately */ private function reset_insertion_mode_appropriately(): void { // Set the first node. $first_node = null; foreach ( $this->state->stack_of_open_elements->walk_down() as $first_node ) { break; } /* * > 1. Let _last_ be false. */ $last = false; foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) { /* * > 2. Let _node_ be the last node in the stack of open elements. * > 3. _Loop_: If _node_ is the first node in the stack of open elements, then set _last_ * > to true, and, if the parser was created as part of the HTML fragment parsing * > algorithm (fragment case), set node to the context element passed to * > that algorithm. * > … */ if ( $node === $first_node ) { $last = true; if ( isset( $this->context_node ) ) { $node = $this->context_node; } } // All of the following rules are for matching HTML elements. if ( 'html' !== $node->namespace ) { continue; } switch ( $node->node_name ) { /* * > 4. If node is a `select` element, run these substeps: * > 1. If _last_ is true, jump to the step below labeled done. * > 2. Let _ancestor_ be _node_. * > 3. _Loop_: If _ancestor_ is the first node in the stack of open elements, * > jump to the step below labeled done. * > 4. Let ancestor be the node before ancestor in the stack of open elements. * > … * > 7. Jump back to the step labeled _loop_. * > 8. _Done_: Switch the insertion mode to "in select" and return. */ case 'SELECT': if ( ! $last ) { foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $ancestor ) { if ( 'html' !== $ancestor->namespace ) { continue; } switch ( $ancestor->node_name ) { /* * > 5. If _ancestor_ is a `template` node, jump to the step below * > labeled _done_. */ case 'TEMPLATE': break 2; /* * > 6. If _ancestor_ is a `table` node, switch the insertion mode to * > "in select in table" and return. */ case 'TABLE': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE; return; } } } $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT; return; /* * > 5. If _node_ is a `td` or `th` element and _last_ is false, then switch the * > insertion mode to "in cell" and return. */ case 'TD': case 'TH': if ( ! $last ) { $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CELL; return; } break; /* * > 6. If _node_ is a `tr` element, then switch the insertion mode to "in row" * > and return. */ case 'TR': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; return; /* * > 7. If _node_ is a `tbody`, `thead`, or `tfoot` element, then switch the * > insertion mode to "in table body" and return. */ case 'TBODY': case 'THEAD': case 'TFOOT': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; return; /* * > 8. If _node_ is a `caption` element, then switch the insertion mode to * > "in caption" and return. */ case 'CAPTION': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION; return; /* * > 9. If _node_ is a `colgroup` element, then switch the insertion mode to * > "in column group" and return. */ case 'COLGROUP': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP; return; /* * > 10. If _node_ is a `table` element, then switch the insertion mode to * > "in table" and return. */ case 'TABLE': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; return; /* * > 11. If _node_ is a `template` element, then switch the insertion mode to the * > current template insertion mode and return. */ case 'TEMPLATE': $this->state->insertion_mode = end( $this->state->stack_of_template_insertion_modes ); return; /* * > 12. If _node_ is a `head` element and _last_ is false, then switch the * > insertion mode to "in head" and return. */ case 'HEAD': if ( ! $last ) { $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD; return; } break; /* * > 13. If _node_ is a `body` element, then switch the insertion mode to "in body" * > and return. */ case 'BODY': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; return; /* * > 14. If _node_ is a `frameset` element, then switch the insertion mode to * > "in frameset" and return. (fragment case) */ case 'FRAMESET': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET; return; /* * > 15. If _node_ is an `html` element, run these substeps: * > 1. If the head element pointer is null, switch the insertion mode to * > "before head" and return. (fragment case) * > 2. Otherwise, the head element pointer is not null, switch the insertion * > mode to "after head" and return. */ case 'HTML': $this->state->insertion_mode = isset( $this->state->head_element ) ? WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD : WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD; return; } } /* * > 16. If _last_ is true, then switch the insertion mode to "in body" * > and return. (fragment case) * * This is only reachable if `$last` is true, as per the fragment parsing case. */ $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; } /** * Runs the adoption agency algorithm. * * @since 6.4.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * * @see https://html.spec.whatwg.org/#adoption-agency-algorithm */ private function run_adoption_agency_algorithm(): void { $budget = 1000; $subject = $this->get_tag(); $current_node = $this->state->stack_of_open_elements->current_node(); if ( // > If the current node is an HTML element whose tag name is subject $current_node && $subject === $current_node->node_name && // > the current node is not in the list of active formatting elements ! $this->state->active_formatting_elements->contains_node( $current_node ) ) { $this->state->stack_of_open_elements->pop(); return; } $outer_loop_counter = 0; while ( $budget-- > 0 ) { if ( $outer_loop_counter++ >= 8 ) { return; } /* * > Let formatting element be the last element in the list of active formatting elements that: * > - is between the end of the list and the last marker in the list, * > if any, or the start of the list otherwise, * > - and has the tag name subject. */ $formatting_element = null; foreach ( $this->state->active_formatting_elements->walk_up() as $item ) { if ( 'marker' === $item->node_name ) { break; } if ( $subject === $item->node_name ) { $formatting_element = $item; break; } } // > If there is no such element, then return and instead act as described in the "any other end tag" entry above. if ( null === $formatting_element ) { $this->bail( 'Cannot run adoption agency when "any other end tag" is required.' ); } // > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return. if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) { $this->state->active_formatting_elements->remove_node( $formatting_element ); return; } // > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return. if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) { return; } /* * > Let furthest block be the topmost node in the stack of open elements that is lower in the stack * > than formatting element, and is an element in the special category. There might not be one. */ $is_above_formatting_element = true; $furthest_block = null; foreach ( $this->state->stack_of_open_elements->walk_down() as $item ) { if ( $is_above_formatting_element && $formatting_element->bookmark_name !== $item->bookmark_name ) { continue; } if ( $is_above_formatting_element ) { $is_above_formatting_element = false; continue; } if ( self::is_special( $item ) ) { $furthest_block = $item; break; } } /* * > If there is no furthest block, then the UA must first pop all the nodes from the bottom of the * > stack of open elements, from the current node up to and including formatting element, then * > remove formatting element from the list of active formatting elements, and finally return. */ if ( null === $furthest_block ) { foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) { $this->state->stack_of_open_elements->pop(); if ( $formatting_element->bookmark_name === $item->bookmark_name ) { $this->state->active_formatting_elements->remove_node( $formatting_element ); return; } } } $this->bail( 'Cannot extract common ancestor in adoption agency algorithm.' ); } $this->bail( 'Cannot run adoption agency when looping required.' ); } /** * Runs the "close the cell" algorithm. * * > Where the steps above say to close the cell, they mean to run the following algorithm: * > 1. Generate implied end tags. * > 2. If the current node is not now a td element or a th element, then this is a parse error. * > 3. Pop elements from the stack of open elements stack until a td element or a th element has been popped from the stack. * > 4. Clear the list of active formatting elements up to the last marker. * > 5. Switch the insertion mode to "in row". * * @see https://html.spec.whatwg.org/multipage/parsing.html#close-the-cell * * @since 6.7.0 */ private function close_cell(): void { $this->generate_implied_end_tags(); // @todo Parse error if the current node is a "td" or "th" element. foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) { $this->state->stack_of_open_elements->pop(); if ( 'TD' === $element->node_name || 'TH' === $element->node_name ) { break; } } $this->state->active_formatting_elements->clear_up_to_last_marker(); $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; } /** * Inserts an HTML element on the stack of open elements. * * @since 6.4.0 * * @see https://html.spec.whatwg.org/#insert-a-foreign-element * * @param WP_HTML_Token $token Name of bookmark pointing to element in original input HTML. */ private function insert_html_element( WP_HTML_Token $token ): void { $this->state->stack_of_open_elements->push( $token ); } /** * Inserts a foreign element on to the stack of open elements. * * @since 6.7.0 * * @see https://html.spec.whatwg.org/#insert-a-foreign-element * * @param WP_HTML_Token $token Insert this token. The token's namespace and * insertion point will be updated correctly. * @param bool $only_add_to_element_stack Whether to skip the "insert an element at the adjusted * insertion location" algorithm when adding this element. */ private function insert_foreign_element( WP_HTML_Token $token, bool $only_add_to_element_stack ): void { $adjusted_current_node = $this->get_adjusted_current_node(); $token->namespace = $adjusted_current_node ? $adjusted_current_node->namespace : 'html'; if ( $this->is_mathml_integration_point() ) { $token->integration_node_type = 'math'; } elseif ( $this->is_html_integration_point() ) { $token->integration_node_type = 'html'; } if ( false === $only_add_to_element_stack ) { /* * @todo Implement the "appropriate place for inserting a node" and the * "insert an element at the adjusted insertion location" algorithms. * * These algorithms mostly impacts DOM tree construction and not the HTML API. * Here, there's no DOM node onto which the element will be appended, so the * parser will skip this step. * * @see https://html.spec.whatwg.org/#insert-an-element-at-the-adjusted-insertion-location */ } $this->insert_html_element( $token ); } /** * Inserts a virtual element on the stack of open elements. * * @since 6.7.0 * * @param string $token_name Name of token to create and insert into the stack of open elements. * @param string|null $bookmark_name Optional. Name to give bookmark for created virtual node. * Defaults to auto-creating a bookmark name. * @return WP_HTML_Token Newly-created virtual token. */ private function insert_virtual_node( $token_name, $bookmark_name = null ): WP_HTML_Token { $here = $this->bookmarks[ $this->state->current_token->bookmark_name ]; $name = $bookmark_name ?? $this->bookmark_token(); $this->bookmarks[ $name ] = new WP_HTML_Span( $here->start, 0 ); $token = new WP_HTML_Token( $name, $token_name, false ); $this->insert_html_element( $token ); return $token; } /* * HTML Specification Helpers */ /** * Indicates if the current token is a MathML integration point. * * @since 6.7.0 * * @see https://html.spec.whatwg.org/#mathml-text-integration-point * * @return bool Whether the current token is a MathML integration point. */ private function is_mathml_integration_point(): bool { $current_token = $this->state->current_token; if ( ! isset( $current_token ) ) { return false; } if ( 'math' !== $current_token->namespace || 'M' !== $current_token->node_name[0] ) { return false; } $tag_name = $current_token->node_name; return ( 'MI' === $tag_name || 'MO' === $tag_name || 'MN' === $tag_name || 'MS' === $tag_name || 'MTEXT' === $tag_name ); } /** * Indicates if the current token is an HTML integration point. * * Note that this method must be an instance method with access * to the current token, since it needs to examine the attributes * of the currently-matched tag, if it's in the MathML namespace. * Otherwise it would be required to scan the HTML and ensure that * no other accounting is overlooked. * * @since 6.7.0 * * @see https://html.spec.whatwg.org/#html-integration-point * * @return bool Whether the current token is an HTML integration point. */ private function is_html_integration_point(): bool { $current_token = $this->state->current_token; if ( ! isset( $current_token ) ) { return false; } if ( 'html' === $current_token->namespace ) { return false; } $tag_name = $current_token->node_name; if ( 'svg' === $current_token->namespace ) { return ( 'DESC' === $tag_name || 'FOREIGNOBJECT' === $tag_name || 'TITLE' === $tag_name ); } if ( 'math' === $current_token->namespace ) { if ( 'ANNOTATION-XML' !== $tag_name ) { return false; } $encoding = $this->get_attribute( 'encoding' ); return ( is_string( $encoding ) && ( 0 === strcasecmp( $encoding, 'application/xhtml+xml' ) || 0 === strcasecmp( $encoding, 'text/html' ) ) ); } $this->bail( 'Should not have reached end of HTML Integration Point detection: check HTML API code.' ); // This unnecessary return prevents tools from inaccurately reporting type errors. return false; } /** * Returns whether an element of a given name is in the HTML special category. * * @since 6.4.0 * * @see https://html.spec.whatwg.org/#special * * @param WP_HTML_Token|string $tag_name Node to check, or only its name if in the HTML namespace. * @return bool Whether the element of the given name is in the special category. */ public static function is_special( $tag_name ): bool { if ( is_string( $tag_name ) ) { $tag_name = strtoupper( $tag_name ); } else { $tag_name = 'html' === $tag_name->namespace ? strtoupper( $tag_name->node_name ) : "{$tag_name->namespace} {$tag_name->node_name}"; } return ( 'ADDRESS' === $tag_name || 'APPLET' === $tag_name || 'AREA' === $tag_name || 'ARTICLE' === $tag_name || 'ASIDE' === $tag_name || 'BASE' === $tag_name || 'BASEFONT' === $tag_name || 'BGSOUND' === $tag_name || 'BLOCKQUOTE' === $tag_name || 'BODY' === $tag_name || 'BR' === $tag_name || 'BUTTON' === $tag_name || 'CAPTION' === $tag_name || 'CENTER' === $tag_name || 'COL' === $tag_name || 'COLGROUP' === $tag_name || 'DD' === $tag_name || 'DETAILS' === $tag_name || 'DIR' === $tag_name || 'DIV' === $tag_name || 'DL' === $tag_name || 'DT' === $tag_name || 'EMBED' === $tag_name || 'FIELDSET' === $tag_name || 'FIGCAPTION' === $tag_name || 'FIGURE' === $tag_name || 'FOOTER' === $tag_name || 'FORM' === $tag_name || 'FRAME' === $tag_name || 'FRAMESET' === $tag_name || 'H1' === $tag_name || 'H2' === $tag_name || 'H3' === $tag_name || 'H4' === $tag_name || 'H5' === $tag_name || 'H6' === $tag_name || 'HEAD' === $tag_name || 'HEADER' === $tag_name || 'HGROUP' === $tag_name || 'HR' === $tag_name || 'HTML' === $tag_name || 'IFRAME' === $tag_name || 'IMG' === $tag_name || 'INPUT' === $tag_name || 'KEYGEN' === $tag_name || 'LI' === $tag_name || 'LINK' === $tag_name || 'LISTING' === $tag_name || 'MAIN' === $tag_name || 'MARQUEE' === $tag_name || 'MENU' === $tag_name || 'META' === $tag_name || 'NAV' === $tag_name || 'NOEMBED' === $tag_name || 'NOFRAMES' === $tag_name || 'NOSCRIPT' === $tag_name || 'OBJECT' === $tag_name || 'OL' === $tag_name || 'P' === $tag_name || 'PARAM' === $tag_name || 'PLAINTEXT' === $tag_name || 'PRE' === $tag_name || 'SCRIPT' === $tag_name || 'SEARCH' === $tag_name || 'SECTION' === $tag_name || 'SELECT' === $tag_name || 'SOURCE' === $tag_name || 'STYLE' === $tag_name || 'SUMMARY' === $tag_name || 'TABLE' === $tag_name || 'TBODY' === $tag_name || 'TD' === $tag_name || 'TEMPLATE' === $tag_name || 'TEXTAREA' === $tag_name || 'TFOOT' === $tag_name || 'TH' === $tag_name || 'THEAD' === $tag_name || 'TITLE' === $tag_name || 'TR' === $tag_name || 'TRACK' === $tag_name || 'UL' === $tag_name || 'WBR' === $tag_name || 'XMP' === $tag_name || // MathML. 'math MI' === $tag_name || 'math MO' === $tag_name || 'math MN' === $tag_name || 'math MS' === $tag_name || 'math MTEXT' === $tag_name || 'math ANNOTATION-XML' === $tag_name || // SVG. 'svg DESC' === $tag_name || 'svg FOREIGNOBJECT' === $tag_name || 'svg TITLE' === $tag_name ); } /** * Returns whether a given element is an HTML Void Element * * > area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr * * @since 6.4.0 * * @see https://html.spec.whatwg.org/#void-elements * * @param string $tag_name Name of HTML tag to check. * @return bool Whether the given tag is an HTML Void Element. */ public static function is_void( $tag_name ): bool { $tag_name = strtoupper( $tag_name ); return ( 'AREA' === $tag_name || 'BASE' === $tag_name || 'BASEFONT' === $tag_name || // Obsolete but still treated as void. 'BGSOUND' === $tag_name || // Obsolete but still treated as void. 'BR' === $tag_name || 'COL' === $tag_name || 'EMBED' === $tag_name || 'FRAME' === $tag_name || 'HR' === $tag_name || 'IMG' === $tag_name || 'INPUT' === $tag_name || 'KEYGEN' === $tag_name || // Obsolete but still treated as void. 'LINK' === $tag_name || 'META' === $tag_name || 'PARAM' === $tag_name || // Obsolete but still treated as void. 'SOURCE' === $tag_name || 'TRACK' === $tag_name || 'WBR' === $tag_name ); } /** * Gets an encoding from a given string. * * This is an algorithm defined in the WHAT-WG specification. * * Example: * * 'UTF-8' === self::get_encoding( 'utf8' ); * 'UTF-8' === self::get_encoding( " \tUTF-8 " ); * null === self::get_encoding( 'UTF-7' ); * null === self::get_encoding( 'utf8; charset=' ); * * @see https://encoding.spec.whatwg.org/#concept-encoding-get * * @todo As this parser only supports UTF-8, only the UTF-8 * encodings are detected. Add more as desired, but the * parser will bail on non-UTF-8 encodings. * * @since 6.7.0 * * @param string $label A string which may specify a known encoding. * @return string|null Known encoding if matched, otherwise null. */ protected static function get_encoding( string $label ): ?string { /* * > Remove any leading and trailing ASCII whitespace from label. */ $label = trim( $label, " \t\f\r\n" ); /* * > If label is an ASCII case-insensitive match for any of the labels listed in the * > table below, then return the corresponding encoding; otherwise return failure. */ switch ( strtolower( $label ) ) { case 'unicode-1-1-utf-8': case 'unicode11utf8': case 'unicode20utf8': case 'utf-8': case 'utf8': case 'x-unicode20utf8': return 'UTF-8'; default: return null; } } /* * Constants that would pollute the top of the class if they were found there. */ /** * Indicates that the next HTML token should be parsed and processed. * * @since 6.4.0 * * @var string */ const PROCESS_NEXT_NODE = 'process-next-node'; /** * Indicates that the current HTML token should be reprocessed in the newly-selected insertion mode. * * @since 6.4.0 * * @var string */ const REPROCESS_CURRENT_NODE = 'reprocess-current-node'; /** * Indicates that the current HTML token should be processed without advancing the parser. * * @since 6.5.0 * * @var string */ const PROCESS_CURRENT_NODE = 'process-current-node'; /** * Indicates that the parser encountered unsupported markup and has bailed. * * @since 6.4.0 * * @var string */ const ERROR_UNSUPPORTED = 'unsupported'; /** * Indicates that the parser encountered more HTML tokens than it * was able to process and has bailed. * * @since 6.4.0 * * @var string */ const ERROR_EXCEEDED_MAX_BOOKMARKS = 'exceeded-max-bookmarks'; /** * Unlock code that must be passed into the constructor to create this class. * * This class extends the WP_HTML_Tag_Processor, which has a public class * constructor. Therefore, it's not possible to have a private constructor here. * * This unlock code is used to ensure that anyone calling the constructor is * doing so with a full understanding that it's intended to be a private API. * * @access private */ const CONSTRUCTOR_UNLOCK_CODE = 'Use WP_HTML_Processor::create_fragment() instead of calling the class constructor directly.'; }