Prevent deletion of post type items that are still used

This is probably an issue may of you have tun into. You embed a post type items into a page or post and then you delete this item without knowing it was embedded somewhere. For this example I will use the popular contact form plugin “Contact Form 7”, but it works with any other post type.

Preparation

We install the Contact Form 7 plugin, create a form and place it into a page using the Gutenberg block. The page’s content will look like this:

<!-- wp:contact-form-7/contact-form-selector {"id":39,"title":"Contact form 1"} -->
<div class="wp-block-contact-form-7-contact-form-selector">[contact-form-7 id="39" title="Contact form 1"]</div>
<!-- /wp:contact-form-7/contact-form-selector -->

We can see the ID for the custom post type wpcf7_contact_form used in the block attributes and the inner shortcode the plugin is still using.

Deleting a post type items

If we now navigate to the “Contact -> Contact Forms” overview we can use the “Bluk actions” to delete the contact form. When we then open the page in the frontend, where the contact form was previously embedded, we get this, quite funny, result:

[contact-form-7 404 "Not Found"]

Instead of rendering the contact form, we will get a “broken shortcode”. For other post types the “error handling” might look differently or even break your site.

Protect post type items from being deleted

So how can we easily prevent deleting such an item if it’s still used somewhere? We would have to search all other post types for this item. If we do that with the “post content” looking for the markup, this will be a very costly operation and for some embeddes it might not easily work. So we will get use some help to make it faster and more reliable.

Using a “helper taxonomy”

We will introduce a taxonomy for this deletion prevention. This taxonomy needs no UI and does not have to be public. These few lines are enough to register it:

function deletion_prevention_register_taxonomy() {
	register_taxonomy(
		'deletion_prevention_tax',
		array( 'post' ),
		array(
			'hierarchical' => false,
			'public'       => false,
		)
	);
}
add_action( 'init', 'deletion_prevention_register_taxonomy' );

We don’t have to register it for every post type we want to use it with, registering it for posts is enough. Now we can add this taxonomy.

Add the helper taxonomy to posts or pages

For our example we parse for all blocks, when a post type is saved. If we find a contact form block, we use the ID of that contact form as the term name:

function deletion_prevention_save_post( $post_ID, $post, $update ) {
	if ( ! in_array( $post->post_type, array( 'post', 'page', true ) ) ) {
		return;
	}

	$blocks = parse_blocks( $post->post_content );

	$forms = array();
	foreach ( $blocks as $block ) {
		if ( 'contact-form-7/contact-form-selector' === $block['blockName'] ) {
			$forms[] = (string) $block['attrs']['id'];
		}
	}

	wp_set_object_terms( $post_ID, $forms, 'deletion_prevention_tax' );
}
add_action( 'save_post', 'deletion_prevention_save_post', 10, 3 );

This callback limits the check for posts and pages only and it only searches for the contact-form-7/contact-form-selector. If you want to prevent the deletion of multiple embedded post types, you might want to use additional taxonomies, other term names or some term meta.

Now that we save this taxonomy, we know exactly where our contact forms are embedded. If we want to delete a contact form, we have to find those posts or pages.

Prevent the deletion

Every time a post type is deleted, some actions are called. One is called just before the deletion is being executed. This is where we hook in:

function deletion_prevention_delete_check( $delete, $post, $force_delete ) {
	deletion_prevention_check( $post );
}
add_action( 'pre_delete_post', 'deletion_prevention_delete_check', 10, 3 );

Some post types can also be send to trash (Contact Form 7 does not support that), so we might also want to prevent trashing of these items:

function deletion_prevention_trash_check( $trash, $post ) {
	deletion_prevention_check( $post );
}
add_action( 'pre_trash_post', 'deletion_prevention_trash_check', 10, 2 );

Now you can see why we are using another new function for the check.

Check for current embeds of the item

Now we are at the final stage. We will search for any embeds of the post type item we are trying to delete or trash by using a taxonomy query on posts and pages using the ID as the term slug:

function deletion_prevention_check( $post ) {
	$args = array(
		'post_type'      => array( 'post', 'page' ),
		'post_status'    => array(
			'publish',
			'pending',
			'draft',
			'future',
			'private',
		),
		'tax_query'      => array(
			array(
				'taxonomy' => 'deletion_prevention_tax',
				'field'    => 'slug',
				'terms'    => (string) $post->ID,
			),
		),
		'posts_per_page' => 1,
		'fields'         => 'ids',

	);
	$posts = get_posts( $args );

	if ( ! empty( $posts ) ) {
		wp_die(
			wp_kses_post(
				sprintf(
					__(
						/* translators: %1$s: link to a filtered posts list, %2$s: link to a filtered pages list */
						'This item as it is still used in <a href="%1$s">posts</a> or <a href="%2$s">pages</a>.',
						'deletion-prevention'
					),
					admin_url( 'edit.php?post_type=post&taxonomy=deletion_prevention_tax&term=' . $post->ID ),
					admin_url( 'edit.php?taxonomy=deletion_prevention_tax&term=' . $post->ID )
				)
			)
		);
	}
}

If your query returns a single post or page, we know that the item is still used. In this case we wp_die to stop the current request which will prevent the deletion of the item. In the message we will also place two links to the posts and pages list, filtered by the embedded item, so we can quickly find out where they are still used, so we can remove them and then safely delete them.

Conclusion

Accidentally deleting a post type item that is still used in other places can happen quite easily and there is currently nothing in core to prevent something like this. Using this approach with a helper taxonomy introduces a simply way to prevent such accidents. The same approach could also be used to prevent setting an embedded item to an non-public status. Something similar would also work preventing the deletion of attachments (although they are also a post type, the same hooks will not work). I hope that this final blog post of 2020 will also help you in some projects. The code from this blog post is available as a plugin in a GIST, so you can try it out yourself or modify it for your needs.

Posted by

Bernhard is a full time web developer who likes to write WordPress plugins in his free time and is an active member of the WP Meetups in Berlin and Potsdam.

Leave a Reply

Your email address will not be published. Required fields are marked *