plate( $request['id'], $this->post_type ); $fields_update = $this->update_additional_fields_for_object( $template, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $request->set_param( 'context', 'edit' ); $post = get_post( $template->wp_id ); /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ do_action( "rest_after_insert_{$this->post_type}", $post, $request, false ); wp_after_insert_post( $post, $update, $post_before ); $response = $this->prepare_item_for_response( $template, $request ); return rest_ensure_response( $response ); } /** * Checks if a given request has access to create a template. * * @since 5.8.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. */ public function create_item_permissions_check( $request ) { return $this->permissions_check( $request ); } /** * Creates a single template. * * @since 5.8.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function create_item( $request ) { $prepared_post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $prepared_post ) ) { return $prepared_post; } $prepared_post->post_name = $request['slug']; $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true ); if ( is_wp_error( $post_id ) ) { if ( 'db_insert_error' === $post_id->get_error_code() ) { $post_id->add_data( array( 'status' => 500 ) ); } else { $post_id->add_data( array( 'status' => 400 ) ); } return $post_id; } $posts = get_block_templates( array( 'wp_id' => $post_id ), $this->post_type ); if ( ! count( $posts ) ) { return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ), array( 'status' => 400 ) ); } $id = $posts[0]->id; $post = get_post( $post_id ); $template = get_block_template( $id, $this->post_type ); $fields_update = $this->update_additional_fields_for_object( $template, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ do_action( "rest_after_insert_{$this->post_type}", $post, $request, true ); wp_after_insert_post( $post, false, null ); $response = $this->prepare_item_for_response( $template, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $template->id ) ) ); return $response; } /** * Checks if a given request has access to delete a single template. * * @since 5.8.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise. */ public function delete_item_permissions_check( $request ) { return $this->permissions_check( $request ); } /** * Deletes a single template. * * @since 5.8.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function delete_item( $request ) { $template = get_block_template( $request['id'], $this->post_type ); if ( ! $template ) { return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); } if ( 'custom' !== $template->source ) { return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.' ), array( 'status' => 400 ) ); } $id = $template->wp_id; $force = (bool) $request['force']; $request->set_param( 'context', 'edit' ); // If we're forcing, then delete permanently. if ( $force ) { $previous = $this->prepare_item_for_response( $template, $request ); $result = wp_delete_post( $id, true ); $response = new WP_REST_Response(); $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data(), ) ); } else { // Otherwise, only trash if we haven't already. if ( 'trash' === $template->status ) { return new WP_Error( 'rest_template_already_trashed', __( 'The template has already been deleted.' ), array( 'status' => 410 ) ); } /* * (Note that internally this falls through to `wp_delete_post()` * if the Trash is disabled.) */ $result = wp_trash_post( $id ); $template->status = 'trash'; $response = $this->prepare_item_for_response( $template, $request ); } if ( ! $result ) { return new WP_Error( 'rest_cannot_delete', __( 'The template cannot be deleted.' ), array( 'status' => 500 ) ); } return $response; } /** * Prepares a single template for create or update. * * @since 5.8.0 * * @param WP_REST_Request $request Request object. * @return stdClass|WP_Error Changes to pass to wp_update_post. */ protected function prepare_item_for_database( $request ) { $template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null; $changes = new stdClass(); if ( null === $template ) { $changes->post_type = $this->post_type; $changes->post_status = 'publish'; $changes->tax_input = array( 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(), ); } elseif ( 'custom' !== $template->source ) { $changes->post_name = $template->slug; $changes->post_type = $this->post_type; $changes->post_status = 'publish'; $changes->tax_input = array( 'wp_theme' => $template->theme, ); $changes->meta_input = array( 'origin' => $template->source, ); } else { $changes->post_name = $template->slug; $changes->ID = $template->wp_id; $changes->post_status = 'publish'; } if ( isset( $request['content'] ) ) { if ( is_string( $request['content'] ) ) { $changes->post_content = $request['content']; } elseif ( isset( $request['content']['raw'] ) ) { $changes->post_content = $request['content']['raw']; } } elseif ( null !== $template && 'custom' !== $template->source ) { $changes->post_content = $template->content; } if ( isset( $request['title'] ) ) { if ( is_string( $request['title'] ) ) { $changes->post_title = $request['title']; } elseif ( ! empty( $request['title']['raw'] ) ) { $changes->post_title = $request['title']['raw']; } } elseif ( null !== $template && 'custom' !== $template->source ) { $changes->post_title = $template->title; } if ( isset( $request['description'] ) ) { $changes->post_excerpt = $request['description']; } elseif ( null !== $template && 'custom' !== $template->source ) { $changes->post_excerpt = $template->description; } if ( 'wp_template' === $this->post_type && isset( $request['is_wp_suggestion'] ) ) { $changes->meta_input = wp_parse_args( array( 'is_wp_suggestion' => $request['is_wp_suggestion'], ), $changes->meta_input = array() ); } if ( 'wp_template_part' === $this->post_type ) { if ( isset( $request['area'] ) ) { $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] ); } elseif ( null !== $template && 'custom' !== $template->source && $template->area ) { $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $template->area ); } elseif ( empty( $template->area ) ) { $changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED; } } if ( ! empty( $request['author'] ) ) { $post_author = (int) $request['author']; if ( get_current_user_id() !== $post_author ) { $user_obj = get_userdata( $post_author ); if ( ! $user_obj ) { return new WP_Error( 'rest_invalid_author', __( 'Invalid author ID.' ), array( 'status' => 400 ) ); } } $changes->post_author = $post_author; } /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ return apply_filters( "rest_pre_insert_{$this->post_type}", $changes, $request ); } /** * Prepare a single template output for response * * @since 5.8.0 * @since 5.9.0 Renamed `$template` to `$item` to match parent class for PHP 8 named parameter support. * @since 6.3.0 Added `modified` property to the response. * * @param WP_Block_Template $item Template instance. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { // Resolve pattern blocks so they don't need to be resolved client-side // in the editor, improving performance. $blocks = parse_blocks( $item->content ); $blocks = resolve_pattern_blocks( $blocks ); $item->content = serialize_blocks( $blocks ); // Restores the more descriptive, specific name for use within this method. $template = $item; $fields = $this->get_fields_for_response( $request ); // Base fields for every template. $data = array(); if ( rest_is_field_included( 'id', $fields ) ) { $data['id'] = $template->id; } if ( rest_is_field_included( 'theme', $fields ) ) { $data['theme'] = $template->theme; } if ( rest_is_field_included( 'content', $fields ) ) { $data['content'] = array(); } if ( rest_is_field_included( 'content.raw', $fields ) ) { $data['content']['raw'] = $template->content; } if ( rest_is_field_included( 'content.block_version', $fields ) ) { $data['content']['block_version'] = block_version( $template->content ); } if ( rest_is_field_included( 'slug', $fields ) ) { $data['slug'] = $template->slug; } if ( rest_is_field_included( 'source', $fields ) ) { $data['source'] = $template->source; } if ( rest_is_field_included( 'origin', $fields ) ) { $data['origin'] = $template->origin; } if ( rest_is_field_included( 'type', $fields ) ) { $data['type'] = $template->type; } if ( rest_is_field_included( 'description', $fields ) ) { $data['description'] = $template->description; } if ( rest_is_field_included( 'title', $fields ) ) { $data['title'] = array(); } if ( rest_is_field_included( 'title.raw', $fields ) ) { $data['title']['raw'] = $template->title; } if ( rest_is_field_included( 'title.rendered', $fields ) ) { if ( $template->wp_id ) { /** This filter is documented in wp-includes/post-template.php */ $data['title']['rendered'] = apply_filters( 'the_title', $template->title, $template->wp_id ); } else { $data['title']['rendered'] = $template->title; } } if ( rest_is_field_included( 'status', $fields ) ) { $data['status'] = $template->status; } if ( rest_is_field_included( 'wp_id', $fields ) ) { $data['wp_id'] = (int) $template->wp_id; } if ( rest_is_field_included( 'has_theme_file', $fields ) ) { $data['has_theme_file'] = (bool) $template->has_theme_file; } if ( rest_is_field_included( 'is_custom', $fields ) && 'wp_template' === $template->type ) { $data['is_custom'] = $template->is_custom; } if ( rest_is_field_included( 'author', $fields ) ) { $data['author'] = (int) $template->author; } if ( rest_is_field_included( 'area', $fields ) && 'wp_template_part' === $template->type ) { $data['area'] = $template->area; } if ( rest_is_field_included( 'modified', $fields ) ) { $data['modified'] = mysql_to_rfc3339( $template->modified ); } if ( rest_is_field_included( 'author_text', $fields ) ) { $data['author_text'] = self::get_wp_templates_author_text_field( $template ); } if ( rest_is_field_included( 'original_source', $fields ) ) { $data['original_source'] = self::get_wp_templates_original_source_field( $template ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { $links = $this->prepare_links( $template->id ); $response->add_links( $links ); if ( ! empty( $links['self']['href'] ) ) { $actions = $this->get_available_actions(); $self = $links['self']['href']; foreach ( $actions as $rel ) { $response->add_link( $rel, $self ); } } } return $response; } /** * Returns the source from where the template originally comes from. * * @since 6.5.0 * * @param WP_Block_Template $template_object Template instance. * @return string Original source of the template one of theme, plugin, site, or user. */ private static function get_wp_templates_original_source_field( $template_object ) { if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { // Added by theme. // Template originally provided by a theme, but customized by a user. // Templates originally didn't have the 'origin' field so identify // older customized templates by checking for no origin and a 'theme' // or 'custom' source. if ( $template_object->has_theme_file && ( 'theme' === $template_object->origin || ( empty( $template_object->origin ) && in_array( $template_object->source, array( 'theme', 'custom', ), true ) ) ) ) { return 'theme'; } // Added by plugin. if ( $template_object->has_theme_file && 'plugin' === $template_object->origin ) { return 'plugin'; } // Added by site. // Template was created from scratch, but has no author. Author support // was only added to templates in WordPress 5.9. Fallback to showing the // site logo and title. if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { return 'site'; } } // Added by user. return 'user'; } /** * Returns a human readable text for the author of the template. * * @since 6.5.0 * * @param WP_Block_Template $template_object Template instance. * @return string Human readable text for the author. */ private static function get_wp_templates_author_text_field( $template_object ) { $original_source = self::get_wp_templates_original_source_field( $template_object ); switch ( $original_source ) { case 'theme': $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); return empty( $theme_name ) ? $template_object->theme : $theme_name; case 'plugin': $plugins = get_plugins(); $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ) ]; return empty( $plugin['Name'] ) ? $template_object->theme : $plugin['Name']; case 'site': return get_bloginfo( 'name' ); case 'user': $author = get_user_by( 'id', $template_object->author ); if ( ! $author ) { return __( 'Unknown author' ); } return $author->get( 'display_name' ); } } /** * Prepares links for the request. * * @since 5.8.0 * * @param integer $id ID. * @return array Links for the given post. */ protected function prepare_links( $id ) { $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $id ) ), ), 'collection' => array( 'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ), ), 'about' => array( 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), ), ); if ( post_type_supports( $this->post_type, 'revisions' ) ) { $template = get_block_template( $id, $this->post_type ); if ( $template instanceof WP_Block_Template && ! empty( $template->wp_id ) ) { $revisions = wp_get_latest_revision_id_and_total_count( $template->wp_id ); $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; $revisions_base = sprintf( '/%s/%s/%s/revisions', $this->namespace, $this->rest_base, $id ); $links['version-history'] = array( 'href' => rest_url( $revisions_base ), 'count' => $revisions_count, ); if ( $revisions_count > 0 ) { $links['predecessor-version'] = array( 'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ), 'id' => $revisions['latest_id'], ); } } } return $links; } /** * Get the link relations available for the post and current user. * * @since 5.8.0 * * @return string[] List of link relations. */ protected function get_available_actions() { $rels = array(); $post_type = get_post_type_object( $this->post_type ); if ( current_user_can( $post_type->cap->publish_posts ) ) { $rels[] = 'https://api.w.org/action-publish'; } if ( current_user_can( 'unfiltered_html' ) ) { $rels[] = 'https://api.w.org/action-unfiltered-html'; } return $rels; } /** * Retrieves the query params for the posts collection. * * @since 5.8.0 * @since 5.9.0 Added `'area'` and `'post_type'`. * * @return array Collection parameters. */ public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 'wp_id' => array( 'description' => __( 'Limit to the specified post id.' ), 'type' => 'integer', ), 'area' => array( 'description' => __( 'Limit to the specified template part area.' ), 'type' => 'string', ), 'post_type' => array( 'description' => __( 'Post type to get the templates for.' ), 'type' => 'string', ), ); } /** * Retrieves the block type' schema, conforming to JSON Schema. * * @since 5.8.0 * @since 5.9.0 Added `'area'`. * * @return array Item schema data. */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'ID of template.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'slug' => array( 'description' => __( 'Unique slug identifying the template.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'required' => true, 'minLength' => 1, 'pattern' => '[a-zA-Z0-9_\%-]+', ), 'theme' => array( 'description' => __( 'Theme identifier for the template.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), ), 'type' => array( 'description' => __( 'Type of template.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), ), 'source' => array( 'description' => __( 'Source of template' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'origin' => array( 'description' => __( 'Source of a customized template' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'content' => array( 'description' => __( 'Content of template.' ), 'type' => array( 'object', 'string' ), 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'properties' => array( 'raw' => array( 'description' => __( 'Content for the template, as it exists in the database.' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'block_version' => array( 'description' => __( 'Version of the content block format used by the template.' ), 'type' => 'integer', 'context' => array( 'edit' ), 'readonly' => true, ), ), ), 'title' => array( 'description' => __( 'Title of template.' ), 'type' => array( 'object', 'string' ), 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'properties' => array( 'raw' => array( 'description' => __( 'Title for the template, as it exists in the database.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), ), 'rendered' => array( 'description' => __( 'HTML title for the template, transformed for display.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), ), ), 'description' => array( 'description' => __( 'Description of template.' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), ), 'status' => array( 'description' => __( 'Status of template.' ), 'type' => 'string', 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), 'default' => 'publish', 'context' => array( 'embed', 'view', 'edit' ), ), 'wp_id' => array( 'description' => __( 'Post ID.' ), 'type' => 'integer', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'has_theme_file' => array( 'description' => __( 'Theme file exists.' ), 'type' => 'bool', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'author' => array( 'description' => __( 'The ID for the author of the template.' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), ), 'modified' => array( 'description' => __( "The date the template was last modified, in the site's timezone." ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'author_text' => array( 'type' => 'string', 'description' => __( 'Human readable text for the author.' ), 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'original_source' => array( 'description' => __( 'Where the template originally comes from e.g. \'theme\'' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), 'enum' => array( 'theme', 'plugin', 'site', 'user', ), ), ), ); if ( 'wp_template' === $this->post_type ) { $schema['properties']['is_custom'] = array( 'description' => __( 'Whether a template is a custom template.' ), 'type' => 'bool', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ); } if ( 'wp_template_part' === $this->post_type ) { $schema['properties']['area'] = array( 'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), ); } $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } } extract_prefix_and_suffix( 'data-wp-bind--src' ) => array( 'data-wp-bind', 'src' ) * extract_prefix_and_suffix( 'data-wp-foo--and--bar' ) => array( 'data-wp-foo', 'and--bar' ) * * @since 6.5.0 * * @param string $directive_name The directive attribute name. * @return array An array containing the directive prefix and optional suffix. */ private function extract_prefix_and_suffix( string $directive_name ): array { return explode( '--', $directive_name, 2 ); } /** * Parses and extracts the namespace and reference path from the given * directive attribute value. * * If the value doesn't contain an explicit namespace, it returns the * default one. If the value contains a JSON object instead of a reference * path, the function tries to parse it and return the resulting array. If * the value contains strings that represent booleans ("true" and "false"), * numbers ("1" and "1.2") or "null", the function also transform them to * regular booleans, numbers and `null`. * * Example: * * extract_directive_value( 'actions.foo', 'myPlugin' ) => array( 'myPlugin', 'actions.foo' ) * extract_directive_value( 'otherPlugin::actions.foo', 'myPlugin' ) => array( 'otherPlugin', 'actions.foo' ) * extract_directive_value( '{ "isOpen": false }', 'myPlugin' ) => array( 'myPlugin', array( 'isOpen' => false ) ) * extract_directive_value( 'otherPlugin::{ "isOpen": false }', 'myPlugin' ) => array( 'otherPlugin', array( 'isOpen' => false ) ) * * @since 6.5.0 * * @param string|true $directive_value The directive attribute value. It can be `true` when it's a boolean * attribute. * @param string|null $default_namespace Optional. The default namespace if none is explicitly defined. * @return array An array containing the namespace in the first item and the JSON, the reference path, or null on the * second item. */ private function extract_directive_value( $directive_value, $default_namespace = null ): array { if ( empty( $directive_value ) || is_bool( $directive_value ) ) { return array( $default_namespace, null ); } // Replaces the value and namespace if there is a namespace in the value. if ( 1 === preg_match( '/^([\w\-_\/]+)::./', $directive_value ) ) { list($default_namespace, $directive_value) = explode( '::', $directive_value, 2 ); } /* * Tries to decode the value as a JSON object. If it fails and the value * isn't `null`, it returns the value as it is. Otherwise, it returns the * decoded JSON or null for the string `null`. */ $decoded_json = json_decode( $directive_value, true ); if ( null !== $decoded_json || 'null' === $directive_value ) { $directive_value = $decoded_json; } return array( $default_namespace, $directive_value ); } /** * Transforms a kebab-case string to camelCase. * * @param string $str The kebab-case string to transform to camelCase. * @return string The transformed camelCase string. */ private function kebab_to_camel_case( string $str ): string { return lcfirst( preg_replace_callback( '/(-)([a-z])/', function ( $matches ) { return strtoupper( $matches[2] ); }, strtolower( rtrim( $str, '-' ) ) ) ); } /** * Processes the `data-wp-interactive` directive. * * It adds the default store namespace defined in the directive value to the * stack so that it's available for the nested interactivity elements. * * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. */ private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { // When exiting tags, it removes the last namespace from the stack. if ( 'exit' === $mode ) { array_pop( $this->namespace_stack ); return; } // Tries to decode the `data-wp-interactive` attribute value. $attribute_value = $p->get_attribute( 'data-wp-interactive' ); /* * Pushes the newly defined namespace or the current one if the * `data-wp-interactive` definition was invalid or does not contain a * namespace. It does so because the function pops out the current namespace * from the stack whenever it finds a `data-wp-interactive`'s closing tag, * independently of whether the previous `data-wp-interactive` definition * contained a valid namespace. */ $new_namespace = null; if ( is_string( $attribute_value ) && ! empty( $attribute_value ) ) { $decoded_json = json_decode( $attribute_value, true ); if ( is_array( $decoded_json ) ) { $new_namespace = $decoded_json['namespace'] ?? null; } else { $new_namespace = $attribute_value; } } $this->namespace_stack[] = ( $new_namespace && 1 === preg_match( '/^([\w\-_\/]+)/', $new_namespace ) ) ? $new_namespace : end( $this->namespace_stack ); } /** * Processes the `data-wp-context` directive. * * It adds the context defined in the directive value to the stack so that * it's available for the nested interactivity elements. * * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. */ private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { // When exiting tags, it removes the last context from the stack. if ( 'exit' === $mode ) { array_pop( $this->context_stack ); return; } $attribute_value = $p->get_attribute( 'data-wp-context' ); $namespace_value = end( $this->namespace_stack ); // Separates the namespace from the context JSON object. list( $namespace_value, $decoded_json ) = is_string( $attribute_value ) && ! empty( $attribute_value ) ? $this->extract_directive_value( $attribute_value, $namespace_value ) : array( $namespace_value, null ); /* * If there is a namespace, it adds a new context to the stack merging the * previous context with the new one. */ if ( is_string( $namespace_value ) ) { $this->context_stack[] = array_replace_recursive( end( $this->context_stack ) !== false ? end( $this->context_stack ) : array(), array( $namespace_value => is_array( $decoded_json ) ? $decoded_json : array() ) ); } else { /* * If there is no namespace, it pushes the current context to the stack. * It needs to do so because the function pops out the current context * from the stack whenever it finds a `data-wp-context`'s closing tag. */ $this->context_stack[] = end( $this->context_stack ); } } /** * Processes the `data-wp-bind` directive. * * It updates or removes the bound attributes based on the evaluation of its * associated reference. * * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. */ private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode ) { $all_bind_directives = $p->get_attribute_names_with_prefix( 'data-wp-bind--' ); foreach ( $all_bind_directives as $attribute_name ) { list( , $bound_attribute ) = $this->extract_prefix_and_suffix( $attribute_name ); if ( empty( $bound_attribute ) ) { return; } $attribute_value = $p->get_attribute( $attribute_name ); $result = $this->evaluate( $attribute_value ); if ( null !== $result && ( false !== $result || ( strlen( $bound_attribute ) > 5 && '-' === $bound_attribute[4] ) ) ) { /* * If the result of the evaluation is a boolean and the attribute is * `aria-` or `data-, convert it to a string "true" or "false". It * follows the exact same logic as Preact because it needs to * replicate what Preact will later do in the client: * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 */ if ( is_bool( $result ) && ( strlen( $bound_attribute ) > 5 && '-' === $bound_attribute[4] ) ) { $result = $result ? 'true' : 'false'; } $p->set_attribute( $bound_attribute, $result ); } else { $p->remove_attribute( $bound_attribute ); } } } } /** * Processes the `data-wp-class` directive. * * It adds or removes CSS classes in the current HTML element based on the * evaluation of its associated references. * * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. */ private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode ) { $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' ); foreach ( $all_class_directives as $attribute_name ) { list( , $class_name ) = $this->extract_prefix_and_suffix( $attribute_name ); if ( empty( $class_name ) ) { return; } $attribute_value = $p->get_attribute( $attribute_name ); $result = $this->evaluate( $attribute_value ); if ( $result ) { $p->add_class( $class_name ); } else { $p->remove_class( $class_name ); } } } } /** * Processes the `data-wp-style` directive. * * It updates the style attribute value of the current HTML element based on * the evaluation of its associated references. * * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. */ private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode ) { $all_style_attributes = $p->get_attribute_names_with_prefix( 'data-wp-style--' ); foreach ( $all_style_attributes as $attribute_name ) { list( , $style_property ) = $this->extract_prefix_and_suffix( $attribute_name ); if ( empty( $style_property ) ) { continue; } $directive_attribute_value = $p->get_attribute( $attribute_name ); $style_property_value = $this->evaluate( $directive_attribute_value ); $style_attribute_value = $p->get_attribute( 'style' ); $style_attribute_value = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : ''; /* * Checks first if the style property is not falsy and the style * attribute value is not empty because if it is, it doesn't need to * update the attribute value. */ if ( $style_property_value || $style_attribute_value ) { $style_attribute_value = $this->merge_style_property( $style_attribute_value, $style_property, $style_property_value ); /* * If the style attribute value is not empty, it sets it. Otherwise, * it removes it. */ if ( ! empty( $style_attribute_value ) ) { $p->set_attribute( 'style', $style_attribute_value ); } else { $p->remove_attribute( 'style' ); } } } } } /** * Merges an individual style property in the `style` attribute of an HTML * element, updating or removing the property when necessary. * * If a property is modified, the old one is removed and the new one is added * at the end of the list. * * @since 6.5.0 * * Example: * * merge_style_property( 'color:green;', 'color', 'red' ) => 'color:red;' * merge_style_property( 'background:green;', 'color', 'red' ) => 'background:green;color:red;' * merge_style_property( 'color:green;', 'color', null ) => '' * * @param string $style_attribute_value The current style attribute value. * @param string $style_property_name The style property name to set. * @param string|false|null $style_property_value The value to set for the style property. With false, null or an * empty string, it removes the style property. * @return string The new style attribute value after the specified property has been added, updated or removed. */ private function merge_style_property( string $style_attribute_value, string $style_property_name, $style_property_value ): string { $style_assignments = explode( ';', $style_attribute_value ); $result = array(); $style_property_value = ! empty( $style_property_value ) ? rtrim( trim( $style_property_value ), ';' ) : null; $new_style_property = $style_property_value ? $style_property_name . ':' . $style_property_value . ';' : ''; // Generates an array with all the properties but the modified one. foreach ( $style_assignments as $style_assignment ) { if ( empty( trim( $style_assignment ) ) ) { continue; } list( $name, $value ) = explode( ':', $style_assignment ); if ( trim( $name ) !== $style_property_name ) { $result[] = trim( $name ) . ':' . trim( $value ) . ';'; } } // Adds the new/modified property at the end of the list. $result[] = $new_style_property; return implode( '', $result ); } /** * Processes the `data-wp-text` directive. * * It updates the inner content of the current HTML element based on the * evaluation of its associated reference. * * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. */ private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode ) { $attribute_value = $p->get_attribute( 'data-wp-text' ); $result = $this->evaluate( $attribute_value ); /* * Follows the same logic as Preact in the client and only changes the * content if the value is a string or a number. Otherwise, it removes the * content. */ if ( is_string( $result ) || is_numeric( $result ) ) { $p->set_content_between_balanced_tags( esc_html( $result ) ); } else { $p->set_content_between_balanced_tags( '' ); } } } /** * Returns the CSS styles for animating the top loading bar in the router. * * @since 6.5.0 * * @return string The CSS styles for the router's top loading bar animation. */ private function get_router_animation_styles(): string { return <<
HTML; } /** * Processes the `data-wp-router-region` directive. * * It renders in the footer a set of HTML elements to notify users about * client-side navigations. More concretely, the elements added are 1) a * top loading bar to visually inform that a navigation is in progress * and 2) an `aria-live` region for accessible navigation announcements. * * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. */ private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode && ! $this->has_processed_router_region ) { $this->has_processed_router_region = true; // Initialize the `core/router` store. $this->state( 'core/router', array( 'navigation' => array( 'texts' => array( 'loading' => __( 'Loading page, please wait.' ), 'loaded' => __( 'Page Loaded.' ), ), ), ) ); // Enqueues as an inline style. wp_register_style( 'wp-interactivity-router-animations', false ); wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() ); wp_enqueue_style( 'wp-interactivity-router-animations' ); // Adds the necessary markup to the footer. add_action( 'wp_footer', array( $this, 'print_router_loading_and_screen_reader_markup' ) ); } } /** * Processes the `data-wp-each` directive. * * This directive gets an array passed as reference and iterates over it * generating new content for each item based on the inner markup of the * `template` tag. * * @since 6.5.0 * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. * @param array $tag_stack The reference to the tag stack. */ private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$tag_stack ) { if ( 'enter' === $mode && 'TEMPLATE' === $p->get_tag() ) { $attribute_name = $p->get_attribute_names_with_prefix( 'data-wp-each' )[0]; $extracted_suffix = $this->extract_prefix_and_suffix( $attribute_name ); $item_name = isset( $extracted_suffix[1] ) ? $this->kebab_to_camel_case( $extracted_suffix[1] ) : 'item'; $attribute_value = $p->get_attribute( $attribute_name ); $result = $this->evaluate( $attribute_value ); // Gets the content between the template tags and leaves the cursor in the closer tag. $inner_content = $p->get_content_between_balanced_template_tags(); // Checks if there is a manual server-side directive processing. $template_end = 'data-wp-each: template end'; $p->set_bookmark( $template_end ); $p->next_tag(); $manual_sdp = $p->get_attribute( 'data-wp-each-child' ); $p->seek( $template_end ); // Rewinds to the template closer tag. $p->release_bookmark( $template_end ); /* * It doesn't process in these situations: * - Manual server-side directive processing. * - Empty or non-array values. * - Associative arrays because those are deserialized as objects in JS. * - Templates that contain top-level texts because those texts can't be * identified and removed in the client. */ if ( $manual_sdp || empty( $result ) || ! is_array( $result ) || ! array_is_list( $result ) || ! str_starts_with( trim( $inner_content ), '<' ) || ! str_ends_with( trim( $inner_content ), '>' ) ) { array_pop( $tag_stack ); return; } // Extracts the namespace from the directive attribute value. $namespace_value = end( $this->namespace_stack ); list( $namespace_value ) = is_string( $attribute_value ) && ! empty( $attribute_value ) ? $this->extract_directive_value( $attribute_value, $namespace_value ) : array( $namespace_value, null ); // Processes the inner content for each item of the array. $processed_content = ''; foreach ( $result as $item ) { // Creates a new context that includes the current item of the array. $this->context_stack[] = array_replace_recursive( end( $this->context_stack ) !== false ? end( $this->context_stack ) : array(), array( $namespace_value => array( $item_name => $item ) ) ); // Processes the inner content with the new context. $processed_item = $this->_process_directives( $inner_content ); if ( null === $processed_item ) { // If the HTML is unbalanced, stop processing it. array_pop( $this->context_stack ); return; } // Adds the `data-wp-each-child` to each top-level tag. $i = new WP_Interactivity_API_Directives_Processor( $processed_item ); while ( $i->next_tag() ) { $i->set_attribute( 'data-wp-each-child', true ); $i->next_balanced_tag_closer_tag(); } $processed_content .= $i->get_updated_html(); // Removes the current context from the stack. array_pop( $this->context_stack ); } // Appends the processed content after the tag closer of the template. $p->append_content_after_template_tag_closer( $processed_content ); // Pops the last tag because it skipped the closing tag of the template tag. array_pop( $tag_stack ); } } }
Fatal error: Uncaught Error: Class 'WP_Interactivity_API' not found in /home/ocb/public_html/wp-includes/interactivity-api/interactivity-api.php:25 Stack trace: #0 /home/ocb/public_html/wp-settings.php(405): wp_interactivity() #1 /home/ocb/public_html/wp-config.php(77): require_once('/home/ocb/publi...') #2 /home/ocb/public_html/wp-load.php(50): require_once('/home/ocb/publi...') #3 /home/ocb/public_html/wp-blog-header.php(13): require_once('/home/ocb/publi...') #4 /home/ocb/public_html/index.php(17): require('/home/ocb/publi...') #5 {main} thrown in /home/ocb/public_html/wp-includes/interactivity-api/interactivity-api.php on line 25

Fatal error: Uncaught Error: Call to a member function set() on null in /home/ocb/public_html/wp-includes/l10n.php:856 Stack trace: #0 /home/ocb/public_html/wp-includes/l10n.php(959): load_textdomain('default', '/home/ocb/publi...', 'lv') #1 /home/ocb/public_html/wp-includes/class-wp-fatal-error-handler.php(49): load_default_textdomain() #2 [internal function]: WP_Fatal_Error_Handler->handle() #3 {main} thrown in /home/ocb/public_html/wp-includes/l10n.php on line 856