Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions includes/blocks/class-convertkit-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ public function register_abilities( $abilities ) {
return array_merge(
$abilities,
array(
new ConvertKit_MCP_Ability_Block_List( $this ),
new ConvertKit_MCP_Ability_Block_Insert( $this ),
new ConvertKit_MCP_Ability_Block_Update( $this ),
new ConvertKit_MCP_Ability_Block_Delete( $this ),
new ConvertKit_MCP_Ability_Content_List( $this ),
new ConvertKit_MCP_Ability_Content_Insert( $this ),
new ConvertKit_MCP_Ability_Content_Update( $this ),
new ConvertKit_MCP_Ability_Content_Delete( $this ),
)
);

Expand Down
279 changes: 279 additions & 0 deletions includes/blocks/helpers/class-convertkit-content-post-helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
<?php
/**
* ConvertKit Content Post Helper class.
*
* @package ConvertKit
* @author ConvertKit
*/

/**
* Mechanism-agnostic helper to find, insert, update and delete a Kit element
* (Broadcast, Form, Form Trigger, Product) within a WordPress Post's content.
*
* This is the entry point used by the Content MCP abilities. It decides how the
* given Post stores its content — Gutenberg blocks, Classic editor / shortcode
* markup — and delegates to the appropriate mechanism-specific helper:
*
* - ConvertKit_Block_Post_Helper for block-based content.
*
* Callers pass an element name (e.g. `form`); this class applies the correct
* prefix for the chosen mechanism (`convertkit/form` for blocks,
* `convertkit_form` for shortcodes).
*
* Page builders (Elementor, etc.) store their content outside post_content and
* are not supported; a WP_Error is returned for posts built with one.
*
* @package ConvertKit
* @author ConvertKit
*/
class ConvertKit_Content_Post_Helper {

/**
* Finds all occurrences of the given Kit element in a Post's content.
*
* @since 3.4.0
*
* @param int $post_id Post ID.
* @param string $element_name Kit Element name (e.g. `form`), without prefix.
* @return WP_Error|bool|array
*/
public static function find( $post_id, $element_name ) {

// Determine how this post stores its content.
$mechanism = self::detect_mechanism( $post_id );
if ( is_wp_error( $mechanism ) ) {
return $mechanism;
}

// Find the element in the post, depending on the mechanism.
// A switch is used as shortcodes and other mechanisms will be supported in the future.
switch ( $mechanism ) {
case 'block':
return ConvertKit_Block_Post_Helper::find(
$post_id,
'convertkit/' . $element_name
);
}

return self::unsupported_mechanism_error( $mechanism );

}

/**
* Inserts a new occurrence of the given Kit Element into a Post's content.
*
* @since 3.4.0
*
* @param int $post_id Post ID.
* @param string $element_name Kit Element name (e.g. `form`), without prefix.
* @param array $attrs Element attributes.
* @param string $position One of 'prepend', 'append', 'index'.
* @param int $index Zero-based top-level index; only used when $position is 'index'.
* @return WP_Error|array
*/
public static function insert( $post_id, $element_name, $attrs, $position = 'append', $index = 0 ) {

// Determine how this post stores its content.
$mechanism = self::detect_mechanism( $post_id );
if ( is_wp_error( $mechanism ) ) {
return $mechanism;
}

// Insert the element into the post, depending on the mechanism.
// A switch is used as shortcodes and other mechanisms will be supported in the future.
switch ( $mechanism ) {
case 'block':
return ConvertKit_Block_Post_Helper::insert(
$post_id,
'convertkit/' . $element_name,
$attrs,
$position,
$index
);
}

return self::unsupported_mechanism_error( $mechanism );

}

/**
* Updates the attributes of an existing occurrence of the given Kit Element
* in a Post's content.
*
* @since 3.4.0
*
* @param int $post_id Post ID.
* @param string $element_name Kit Element name (e.g. `form`), without prefix.
* @param int $occurrence_index Zero-based occurrence index to update.
* @param array $attrs Element attributes.
* @return WP_Error|array
*/
public static function update( $post_id, $element_name, $occurrence_index, $attrs ) {

// Determine how this post stores its content.
$mechanism = self::detect_mechanism( $post_id );
if ( is_wp_error( $mechanism ) ) {
return $mechanism;
}

// Updates the existing occurrence of the element in the post, depending on the mechanism.
// A switch is used as shortcodes and other mechanisms will be supported in the future.
switch ( $mechanism ) {
case 'block':
return ConvertKit_Block_Post_Helper::update(
$post_id,
'convertkit/' . $element_name,
$occurrence_index,
$attrs
);
}

return self::unsupported_mechanism_error( $mechanism );

}

/**
* Deletes a specific occurrence of the given Kit Element from a Post's
* content.
*
* @since 3.4.0
*
* @param int $post_id Post ID.
* @param string $element_name Kit Element name (e.g. `form`), without prefix.
* @param int $occurrence_index Zero-based occurrence index to delete.
* @return WP_Error|array
*/
public static function delete( $post_id, $element_name, $occurrence_index ) {

// Determine how this post stores its content.
$mechanism = self::detect_mechanism( $post_id );
if ( is_wp_error( $mechanism ) ) {
return $mechanism;
}

// Delete the element from the post, depending on the mechanism.
// A switch is used as shortcodes and other mechanisms will be supported in the future.
switch ( $mechanism ) {
case 'block':
return ConvertKit_Block_Post_Helper::delete(
$post_id,
'convertkit/' . $element_name,
$occurrence_index
);
}

return self::unsupported_mechanism_error( $mechanism );

}

/**
* Determines how the given Post stores its content.
*
* Returns one of:
* - 'block' The Post uses Gutenberg blocks.
* - 'shortcode' The Post uses Classic editor / shortcode markup.
* - WP_Error The Post does not exist, or is built with an unsupported
* page builder.
*
* Page builders are checked first, because a page builder typically leaves
* post_content empty (or a non-authoritative fallback) and stores its real
* content in post meta. Writing a block or shortcode into post_content for
* such a Post would have no visible effect, so we refuse rather than fail
* silently.
*
* @since 3.4.0
*
* @param int $post_id Post ID.
* @return string|WP_Error
*/
private static function detect_mechanism( $post_id ) {

// Get Post.
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error(
'convertkit_content_post_helper_post_not_found',
/* translators: %d: post ID */
sprintf( __( 'No post exists with ID %d.', 'convertkit' ), $post_id )
);
}

// Bail if the Post is built with a page builder, as these store their
// content outside post_content.
$page_builder = self::detect_page_builder( $post_id );
if ( $page_builder ) {
return new WP_Error(
'convertkit_content_post_helper_page_builder_unsupported',
sprintf(
/* translators: %s: page builder name */
__( 'This content is built with %s, which is not yet supported. Add the Kit Element using the page builder editor instead.', 'convertkit' ),
$page_builder
)
);
}

// Block-based content if the Post contains block markup; otherwise
// treat it as Classic editor / shortcode content. An empty Post also
// falls through to 'shortcode' — a shortcode renders correctly in
// both Classic and block editors, so this is a safe default.
return has_blocks( $post->post_content ) ? 'block' : 'shortcode';

}

/**
* Returns the human-readable name of the page builder used to build the
* given Post, or false if no supported page builder is detected.
*
* @since 3.4.0
*
* @param int $post_id Post ID.
* @return string|false
*/
private static function detect_page_builder( $post_id ) {

// Elementor stores its content in the _elementor_data post meta key,
// and flags edited posts via _elementor_edit_mode.
if ( 'builder' === get_post_meta( $post_id, '_elementor_edit_mode', true ) ) {
return 'Elementor';
}

/**
* Filters the detected page builder for a Post.
*
* Return a non-empty string (the page builder's name) to mark the Post
* as built with an unsupported page builder, causing the Content MCP
* abilities to return an error rather than writing to post_content.
*
* @since 3.4.0
*
* @param string|false $page_builder Detected page builder name, or false.
* @param int $post_id Post ID.
*/
return apply_filters( 'convertkit_content_post_helper_detect_page_builder', false, $post_id );

}

/**
* Returns a WP_Error for an unrecognised content mechanism. Acts as a
* defensive fallback; detect_mechanism() should only ever return a known
* mechanism or a WP_Error.
*
* @since 3.4.0
*
* @param string $mechanism The unrecognised mechanism.
* @return WP_Error
*/
private static function unsupported_mechanism_error( $mechanism ) {

return new WP_Error(
'convertkit_content_post_helper_unsupported_mechanism',
sprintf(
/* translators: %s: mechanism identifier */
__( 'Unsupported content mechanism: %s.', 'convertkit' ),
$mechanism
)
);

}

}
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<?php
/**
* Kit MCP Ability: Delete a block from a post.
* Kit MCP Ability: Delete a Kit Element from a post.
*
* @package ConvertKit
* @author ConvertKit
*/

/**
* Ability that removes an occurrence of a Kit block from a WordPress post's
* content.
* Ability that removes a single occurrence of a Kit element
* (Broadcast, Form, Form Trigger, Product) from a WordPress Post's content.
*
* Registered by a block opting in via the `convertkit_abilities` filter and
* produces an ability named `kit/<block-name>-delete` (e.g. `kit/form-delete`).
* Registered by an element opting in via the `convertkit_abilities` filter and
* produces an ability named `kit/<element>-delete` (e.g. `kit/form-delete`).
*
* @package ConvertKit
* @author ConvertKit
*/
class ConvertKit_MCP_Ability_Block_Delete extends ConvertKit_MCP_Ability_Block {
class ConvertKit_MCP_Ability_Content_Delete extends ConvertKit_MCP_Ability_Content {

/**
* Sets whether the ability is destructive.
Expand Down Expand Up @@ -51,7 +51,7 @@ public function get_label() {

return sprintf(
/* translators: %s: block title */
__( 'Delete an existing %s block from a post', 'convertkit' ),
__( 'Delete an existing %s element from a post', 'convertkit' ),
$this->block->get_title()
);

Expand All @@ -68,7 +68,7 @@ public function get_description() {

return sprintf(
/* translators: 1: block full name e.g. convertkit/form, 2: block title */
__( 'Removes a single occurrence of the %1$s (%2$s) block from the given post.', 'convertkit' ),
__( 'Removes a single occurrence of the %1$s (%2$s) element from the given post.', 'convertkit' ),
'convertkit/' . $this->block->get_name(),
$this->block->get_title()
);
Expand All @@ -91,12 +91,12 @@ public function get_input_schema() {
'post_id' => array(
'type' => 'integer',
'minimum' => 1,
'description' => __( 'ID of the post containing the block.', 'convertkit' ),
'description' => __( 'ID of the post containing the element.', 'convertkit' ),
),
'occurrence_index' => array(
'type' => 'integer',
'minimum' => 0,
'description' => __( 'The zero-based occurrence index of the block to delete.', 'convertkit' ),
'description' => __( 'The zero-based occurrence index of the element to delete.', 'convertkit' ),
),
),
);
Expand Down Expand Up @@ -127,8 +127,8 @@ public function execute_callback( $input ) {
// Get occurrence index.
$occurrence_index = isset( $input['occurrence_index'] ) ? (int) $input['occurrence_index'] : 0;

// Delete block from post.
return ConvertKit_Block_Post_Helper::delete( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index );
// Delete the element from the post.
return ConvertKit_Content_Post_Helper::delete( $post_id, $this->block->get_name(), $occurrence_index );

}

Expand Down
Loading
Loading