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' );