add_cap( MICROEDIT_CAP ); } } } register_activation_hook( __FILE__, 'microedit_activate' ); /** * Returns the queried post when the current user may microedit it. * * @return WP_Post|null */ function microedit_current_post() { static $post = false; if ( false !== $post ) { return $post; } $post = null; if ( is_admin() || is_feed() || is_preview() || ! is_singular() || is_embed() ) { return null; } $candidate = get_queried_object(); if ( $candidate instanceof WP_Post && microedit_can_edit( $candidate ) && ! post_password_required( $candidate ) ) { $post = $candidate; } return $post; } /** * Checks capability, post access and editor support for a post. * * @param WP_Post $post Post. * @return bool */ function microedit_can_edit( WP_Post $post ) { /** * Filters the post types Microedit works on. * * @param string[] $post_types Post type names. */ $post_types = apply_filters( 'microedit_post_types', get_post_types_by_support( 'editor' ) ); return current_user_can( MICROEDIT_CAP ) && current_user_can( 'edit_post', $post->ID ) && in_array( $post->post_type, (array) $post_types, true ) && '' !== trim( $post->post_content ); } /** * Data attributes that mark the rendered content of the post. * * @param WP_Post $post Post. * @return array */ function microedit_body_attributes( WP_Post $post ) { return array( 'data-microedit-post-id' => (string) $post->ID, ); } /** * Block themes: mark the core/post-content wrapper of the queried post. * * Wrapping the content in an extra element would break the theme's * constrained layout, so the existing wrapper receives the attributes. * * @param string $content Rendered block. * @param array $block Parsed block. * @param WP_Block $instance Block instance. * @return string */ function microedit_mark_post_content_block( $content, $block, $instance ) { $post = microedit_current_post(); if ( ! $post || (int) ( $instance->context['postId'] ?? 0 ) !== $post->ID || '' === trim( $content ) ) { return $content; } $processor = new WP_HTML_Tag_Processor( $content ); if ( ! $processor->next_tag() || $processor->get_attribute( 'data-microedit-post-id' ) ) { return $content; } $processor->add_class( 'microedit-body' ); foreach ( microedit_body_attributes( $post ) as $name => $value ) { $processor->set_attribute( $name, $value ); } microedit_enqueue( $post ); return $processor->get_updated_html(); } add_filter( 'render_block_core/post-content', 'microedit_mark_post_content_block', 10, 3 ); /** * Classic themes: wrap the main post's rendered content. * * @param string $content Rendered content. * @return string */ function microedit_wrap_content( $content ) { static $done = false; $post = microedit_current_post(); if ( $done || ! $post || wp_is_block_theme() || ! in_the_loop() || ! is_main_query() || get_the_ID() !== $post->ID ) { return $content; } $done = true; $attributes = ''; foreach ( microedit_body_attributes( $post ) as $name => $value ) { $attributes .= ' ' . $name . '="' . esc_attr( $value ) . '"'; } microedit_enqueue( $post ); return '
' . $content . '
'; } add_filter( 'the_content', 'microedit_wrap_content', 9999 ); /** * Enqueues the editor, the UI and the stored-block lookup data. * * @param WP_Post $post Post. */ function microedit_enqueue( WP_Post $post ) { static $done = false; if ( $done ) { return; } $done = true; $source = Microedit_Source::source( $post->post_content ); $blocks = array(); foreach ( Microedit_Source::blocks( $source ) as $block ) { $blocks[] = array( 'tag' => $block['tag'], 'text' => $block['text'], ); } if ( ! $blocks ) { return; } wp_enqueue_editor(); $url = plugin_dir_url( __FILE__ ); wp_enqueue_style( 'microedit', $url . 'css/microedit.css', array( 'editor-buttons' ), MICROEDIT_VERSION ); wp_enqueue_script( 'microedit', $url . 'js/microedit.js', array( 'jquery', 'wp-i18n', 'editor' ), MICROEDIT_VERSION, true ); wp_set_script_translations( 'microedit', 'microedit' ); wp_localize_script( 'microedit', 'microeditSettings', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'microedit' ), 'posts' => array( (string) $post->ID => array( 'hash' => microedit_hash( $post ), 'blocks' => $blocks, ), ), ) ); } /** * Fingerprint of the stored content, used to reject concurrent changes. * * @param WP_Post $post Post. * @return string */ function microedit_hash( WP_Post $post ) { return hash( 'sha256', $post->post_content ); } /** * Validates an AJAX request and locates the requested block. * * @return array Keys: post, source, found, selected. */ function microedit_ajax_request() { if ( ! check_ajax_referer( 'microedit', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'Your session has expired. Reload the page and try again.', 'microedit' ) ), 403 ); } $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; $index = isset( $_POST['block_index'] ) ? filter_var( wp_unslash( $_POST['block_index'] ), FILTER_VALIDATE_INT ) : false; $tag = isset( $_POST['block_tag'] ) ? strtolower( sanitize_key( wp_unslash( $_POST['block_tag'] ) ) ) : ''; $hash = isset( $_POST['hash'] ) ? sanitize_text_field( wp_unslash( $_POST['hash'] ) ) : ''; $selected = isset( $_POST['selected'] ) ? (string) wp_unslash( $_POST['selected'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- compared as text only. if ( ! $post_id || false === $index || $index < 0 || ! in_array( $tag, Microedit_Source::EDITABLE_TAGS, true ) || '' === trim( $selected ) || mb_strlen( $selected ) > 3000 ) { wp_send_json_error( array( 'message' => __( 'Invalid text block selection.', 'microedit' ) ), 400 ); } $post = get_post( $post_id ); if ( ! $post || ! microedit_can_edit( $post ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to edit this text.', 'microedit' ) ), 403 ); } if ( ! hash_equals( microedit_hash( $post ), $hash ) ) { wp_send_json_error( array( 'message' => __( 'The post changed after this page was loaded. Reload the page and try again.', 'microedit' ) ), 409 ); } $lock = microedit_post_lock( $post->ID ); if ( $lock ) { $user = get_userdata( $lock ); wp_send_json_error( array( /* translators: %s: user display name. */ 'message' => sprintf( __( '%s is currently editing this post.', 'microedit' ), $user ? $user->display_name : __( 'Another user', 'microedit' ) ), ), 423 ); } $source = Microedit_Source::source( $post->post_content ); $found = Microedit_Source::find( $source, $index, $tag, $selected ); if ( null === $found ) { wp_send_json_error( array( 'message' => __( 'The chosen block could not be matched safely to the stored post content.', 'microedit' ) ), 400 ); } return compact( 'post', 'source', 'found', 'selected' ); } /** * Wrapper for wp_check_post_lock(), which lives in an admin include. * * @param int $post_id Post ID. * @return int|false */ function microedit_post_lock( $post_id ) { if ( ! function_exists( 'wp_check_post_lock' ) ) { require_once ABSPATH . 'wp-admin/includes/post.php'; } return wp_check_post_lock( $post_id ); } /** * AJAX: returns the complete stored block for the editor. */ function microedit_ajax_load() { $request = microedit_ajax_request(); wp_send_json_success( array( 'html' => Microedit_Source::editor_html( $request['source'], $request['found'] ), ) ); } add_action( 'wp_ajax_microedit_load', 'microedit_ajax_load' ); /** * AJAX: replaces the block and saves the post. */ function microedit_ajax_save() { $request = microedit_ajax_request(); $replacement = isset( $_POST['content'] ) ? (string) wp_unslash( $_POST['content'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce checked in microedit_ajax_request(); kses runs in wp_update_post() for users without unfiltered_html. $text = Microedit_Source::fold( html_entity_decode( wp_strip_all_tags( $replacement ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ); if ( '' === $text && ! preg_match( '/<(?:img|hr|iframe|video|audio)\b/i', $replacement ) ) { wp_send_json_error( array( 'message' => __( 'Enter some replacement text.', 'microedit' ) ), 400 ); } $updated = Microedit_Source::replace( $request['source'], $request['found'], $replacement ); if ( is_wp_error( $updated ) ) { wp_send_json_error( array( 'message' => $updated->get_error_message() ), 400 ); } $result = wp_update_post( wp_slash( array( 'ID' => $request['post']->ID, 'post_content' => $updated, ) ), true ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ), 500 ); } wp_send_json_success( array( 'text' => $text ) ); } add_action( 'wp_ajax_microedit_save', 'microedit_ajax_save' );