diff --git a/includes/blocks/helpers/class-convertkit-block-post-helper.php b/includes/blocks/helpers/class-convertkit-block-post-helper.php new file mode 100644 index 000000000..3fcaaab40 --- /dev/null +++ b/includes/blocks/helpers/class-convertkit-block-post-helper.php @@ -0,0 +1,301 @@ +post_content ); + $found = array(); + + $occurrence_index = 0; + + foreach ( $blocks as $index => $block ) { + if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { + continue; + } + + $found[] = array( + 'index' => (int) $index, + 'occurrence_index' => (int) $occurrence_index, + 'attrs' => $block['attrs'], + ); + + ++$occurrence_index; + } + + // If no blocks found, return false. + if ( empty( $found ) ) { + return false; + } + + return $found; + + } + + /** + * Inserts a new block into the Post's content at the specified position. + * + * @since 3.4.0 + * + * @param int $post_id Post ID. + * @param string $block_name Programmatic Block Name. + * @param array $attrs Block Attributes. + * @param string $position One of 'prepend', 'append', 'index'. + * @param int $index Zero-based top-level block index; only used when $position is 'index'. + * @return WP_Error|array + */ + public static function insert( $post_id, $block_name, $attrs, $position = 'append', $index = 0 ) { + + // Get Post. + $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'convertkit_block_post_helper_insert_block_post_not_found', + /* translators: %d: Post ID */ + sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) + ); + } + + // Parse blocks. + $blocks = parse_blocks( $post->post_content ); + + // Build the new block to insert. + $new_block = array( + 'blockName' => $block_name, + 'attrs' => (array) $attrs, + 'innerBlocks' => array(), + 'innerHTML' => '', + 'innerContent' => array(), + ); + + // Resolve $position into a concrete zero-based splice point in the + // top-level block array. + switch ( $position ) { + case 'prepend': + $insert_at = 0; + break; + + case 'index': + $insert_at = max( 0, min( (int) $index, count( $blocks ) ) ); + break; + + case 'append': + default: + $insert_at = count( $blocks ); + break; + } + + // Splice in the new block. + array_splice( $blocks, $insert_at, 0, array( $new_block ) ); + + // Update Post. + $result = wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => serialize_blocks( $blocks ), + ), + true + ); + + // Bail if the update failed. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return the index the block was inserted at. + return array( + 'post_id' => $post_id, + 'index' => $insert_at, + ); + + } + + /** + * Updates the attributes of an existing block in the Post's content. + * + * @since 3.4.0 + * + * @param int $post_id Post ID. + * @param string $block_name Programmatic Block Name. + * @param int $occurrence_index Position to update block. + * @param array $attrs Block Attributes. + * @return WP_Error|array + */ + public static function update( $post_id, $block_name, $occurrence_index, $attrs ) { + + // Get Post. + $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'convertkit_block_post_helper_update_block_post_not_found', + /* translators: %d: post ID */ + sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) + ); + } + + // Parse blocks. + $blocks = parse_blocks( $post->post_content ); + $update_at = 0; + $block_index = 0; + $matched = false; + + foreach ( $blocks as $key => $block ) { + ++$update_at; + + // Skip if the block name does not match. + if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { + continue; + } + + // Update the block if the occurrence index matches. + if ( $block_index === (int) $occurrence_index ) { + $blocks[ $key ]['attrs'] = array_merge( (array) $block['attrs'], (array) $attrs ); + $matched = true; + break; + } + + ++$block_index; + } + + // Bail if the block was not found. + if ( ! $matched ) { + return new WP_Error( + 'convertkit_block_post_helper_occurrence_not_found', + /* translators: 1: block name, 2: occurrence index, 3: post ID */ + sprintf( __( 'No occurrence #%2$d of block %1$s found in post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) + ); + } + + // Update Post. + $result = wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => serialize_blocks( $blocks ), + ), + true + ); + + // Bail if the update failed. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return the index the block was updated at. + return array( + 'post_id' => $post_id, + 'index' => ( $update_at - 1 ), + ); + + } + + /** + * Deletes a specific block from the Post's content. + * + * @since 3.4.0 + * + * @param int $post_id Post ID. + * @param string $block_name Programmatic Block Name. + * @param int $occurrence_index Zero-based index among this block's occurrences in the post. + * @return WP_Error|array + */ + public static function delete( $post_id, $block_name, $occurrence_index ) { + + // Get Post. + $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'convertkit_block_post_helper_update_block_post_not_found', + /* translators: %d: post ID */ + sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) + ); + } + + // Parse blocks. + $blocks = parse_blocks( $post->post_content ); + $delete_at = 0; + $block_index = 0; + $matched = false; + + foreach ( $blocks as $key => $block ) { + ++$delete_at; + + // Skip if the block name does not match. + if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { + continue; + } + + // Update the block if the occurrence index matches. + if ( $block_index === (int) $occurrence_index ) { + unset( $blocks[ $key ] ); + $blocks = array_values( $blocks ); + $matched = true; + break; + } + + ++$block_index; + } + + // Bail if the block was not found. + if ( ! $matched ) { + return new WP_Error( + 'convertkit_block_post_helper_occurrence_not_found', + /* translators: 1: block name, 2: occurrence index, 3: post ID */ + sprintf( __( 'No occurrence #%2$d of block %1$s found in post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) + ); + } + + // Update Post. + $result = wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => serialize_blocks( $blocks ), + ), + true + ); + + // Bail if the update failed. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return the index the block was deleted from. + return array( + 'post_id' => $post_id, + 'index' => ( $delete_at - 1 ), + ); + + } + +} diff --git a/tests/Integration/BlockPostHelperTest.php b/tests/Integration/BlockPostHelperTest.php new file mode 100644 index 000000000..207a20d75 --- /dev/null +++ b/tests/Integration/BlockPostHelperTest.php @@ -0,0 +1,441 @@ +postID = $this->createPost(); + } + + /** + * Performs actions after each test. + * + * @since 3.4.0 + */ + public function tearDown(): void + { + // Deactivate Plugin. + deactivate_plugins('convertkit/wp-convertkit.php'); + + parent::tearDown(); + } + + /** + * Test that the find() method returns the correct block indicies and attributes. + * + * @since 3.4.0 + */ + public function testFind() + { + // Find the block. + $blocks = \ConvertKit_Block_Post_Helper::find( $this->postID, 'convertkit/form' ); + $this->assertIsArray( $blocks ); + $this->assertCount( 2, $blocks ); + + // Assert first matching block indicies and attributes are correct. + $this->assertEquals( $this->formBlockIndices[0], $blocks[0]['index'] ); + $this->assertEquals( 0, $blocks[0]['occurrence_index'] ); + $this->assertEquals( $_ENV['CONVERTKIT_API_FORM_ID'], $blocks[0]['attrs']['form'] ); + + // Assert second matching block indicies and attributes are correct. + $this->assertEquals( $this->formBlockIndices[1], $blocks[1]['index'] ); + $this->assertEquals( 1, $blocks[1]['occurrence_index'] ); + $this->assertEquals( $_ENV['CONVERTKIT_API_FORM_ID'], $blocks[1]['attrs']['form'] ); + } + + /** + * Test that the find() method returns false when no blocks match the given block name. + * + * @since 3.4.0 + */ + public function testFindWhenNoBlocksMatch() + { + $this->assertFalse(\ConvertKit_Block_Post_Helper::find( $this->postID, 'fake/block' )); + } + + /** + * Test that the find() method returns a WP_Error when the post does not exist. + * + * @since 3.4.0 + */ + public function testFindWhenPostDoesNotExist() + { + $this->assertInstanceOf(\WP_Error::class, \ConvertKit_Block_Post_Helper::find( 999999, 'convertkit/form' )); + } + + /** + * Test that the insert() method inserts a new block at the beginning of the content + * when the position is set to prepend. + * + * @since 3.4.0 + */ + public function testInsertPrepend() + { + $result = \ConvertKit_Block_Post_Helper::insert( + post_id: $this->postID, + block_name: 'convertkit/form', + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ], + position: 'prepend' + ); + + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( 0, $result['index'] ); + } + + /** + * Test that the insert() method inserts a new block at the end of the content + * when the position is set to append. + * + * @since 3.4.0 + */ + public function testInsertAppend() + { + $result = \ConvertKit_Block_Post_Helper::insert( + post_id: $this->postID, + block_name: 'convertkit/form', + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ], + position: 'append' + ); + + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( $this->totalBlocks + 1, $result['index'] ); + } + + /** + * Test that the insert() method inserts a new block at the specified index position. + * + * @since 3.4.0 + */ + public function testInsertIndex() + { + $result = \ConvertKit_Block_Post_Helper::insert( + post_id: $this->postID, + block_name: 'convertkit/form', + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ], + position: 'index', + index: 1 + ); + + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( 1, $result['index'] ); + } + + /** + * Test that the insert() method inserts a new block at end of the content when + * the index is out of bounds. + * + * @since 3.4.0 + */ + public function testInsertIndexOutOfBounds() + { + $result = \ConvertKit_Block_Post_Helper::insert( + post_id: $this->postID, + block_name: 'convertkit/form', + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ], + position: 'index', + index: 100 + ); + + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( $this->totalBlocks + 1, $result['index'] ); + } + + /** + * Test that the insert() method inserts a new block at the beginning of the content when + * the index is negative. + * + * @since 3.4.0 + */ + public function testInsertIndexNegative() + { + $result = \ConvertKit_Block_Post_Helper::insert( + post_id: $this->postID, + block_name: 'convertkit/form', + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ], + position: 'index', + index: -1 + ); + + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( 0, $result['index'] ); + } + + /** + * Test that the insert() method returns a WP_Error when the post does not exist. + * + * @since 3.4.0 + */ + public function testInsertWhenPostDoesNotExist() + { + $result = \ConvertKit_Block_Post_Helper::insert( + post_id: 999999, + block_name: 'convertkit/form', + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ], + position: 'index', + index: 0 + ); + $this->assertInstanceOf(\WP_Error::class, $result ); + } + + /** + * Test that the update() method updates the attributes of an existing block. + * + * @since 3.4.0 + */ + public function testUpdate() + { + $result = \ConvertKit_Block_Post_Helper::update( + post_id: $this->postID, + block_name: 'convertkit/form', + occurrence_index: 0, + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ] + ); + + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( $this->formBlockIndices[0], $result['index'] ); + + $result = \ConvertKit_Block_Post_Helper::update( + post_id: $this->postID, + block_name: 'convertkit/form', + occurrence_index: 1, + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ] + ); + + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( $this->formBlockIndices[1], $result['index'] ); + } + + /** + * Test that the update() method returns a WP_Error when the occurrence index is out of bounds. + * + * @since 3.4.0 + */ + public function testUpdateWhenOccurrenceIndexIsOutOfBounds() + { + $result = \ConvertKit_Block_Post_Helper::update( + post_id: $this->postID, + block_name: 'convertkit/form', + occurrence_index: 999, + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ] + ); + $this->assertInstanceOf(\WP_Error::class, $result ); + } + + /** + * Test that the update() method returns a WP_Error when the post does not exist. + * + * @since 3.4.0 + */ + public function testUpdateWhenPostDoesNotExist() + { + $result = \ConvertKit_Block_Post_Helper::update( + post_id: 999999, + block_name: 'convertkit/form', + occurrence_index: 0, + attrs: [ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'] ] + ); + $this->assertInstanceOf(\WP_Error::class, $result ); + } + + /** + * Test that the delete() method deletes an existing block. + * + * @since 3.4.0 + */ + public function testDelete() + { + $result = \ConvertKit_Block_Post_Helper::delete( + post_id: $this->postID, + block_name: 'convertkit/form', + occurrence_index: 1 + ); + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( $this->formBlockIndices[1], $result['index'] ); + + $result = \ConvertKit_Block_Post_Helper::delete( + post_id: $this->postID, + block_name: 'convertkit/form', + occurrence_index: 0 + ); + $this->assertIsArray( $result ); + $this->assertEquals( $this->postID, $result['post_id'] ); + $this->assertEquals( $this->formBlockIndices[0], $result['index'] ); + } + + /** + * Test that the delete() method returns a WP_Error when the occurrence index is out of bounds. + * + * @since 3.4.0 + */ + public function testDeleteWhenOccurrenceIndexIsOutOfBounds() + { + $result = \ConvertKit_Block_Post_Helper::delete( + post_id: $this->postID, + block_name: 'convertkit/form', + occurrence_index: 999 + ); + $this->assertInstanceOf(\WP_Error::class, $result ); + } + + /** + * Test that the delete() method returns a WP_Error when the post does not exist. + * + * @since 3.4.0 + */ + public function testDeleteWhenPostDoesNotExist() + { + $result = \ConvertKit_Block_Post_Helper::delete( + post_id: 999999, + block_name: 'convertkit/form', + occurrence_index: 0 + ); + $this->assertInstanceOf(\WP_Error::class, $result ); + } + + /** + * Mocks a post for testing. + * + * @since 3.4.0 + * @return int + */ + private function createPost() + { + // Create a Post with the given block. + return $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Block Post', + 'post_content' => ' +
Item #1
+ + + +Item #2: Adhaésionés altéram improbis mi pariendarum sit stulti triarium
+ + + +Item #3
+ + + +Item #4
+ + + +Item #5
+ + + +