Debugging memory usage

Last week, an issue was reported for a site. It only occurred sometimes and the result was an Error 500. Activating the WP_DEBUG mode I was quickly able to find out that the memory limit was hit. The only fix was an increase of this limit, as the functionality on the pages used quite a lot memory and an optimization was not easy to find.

In those cases it would be nice to be able to find out how much you have to increase the limit. So how much would a page load usually take. There are several ways to find this out.

The danegerous way

One simple solution would be to decrease the memory limit until you always get a memory limit error. Then you slightly increase the limit until you only get the error some times. This is the value the actual usage would likely be. Then you add some more to that limit, so you don’t run into it again.

Read more →

Set the debug level using error_reporting

Everyone knows that. You work on a website and you run into issues. Sometime you even get a critical error like this:

But in some cases, you don’t get the error on every single request. Then you have to activate the debug mode and log all errors.

Activating the debug mode

To activate the debug mode for WordPress you simply have to set some constants in your wp-config.php file. These are the defaults:

Read more →

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.

Extract strings from Gutenberg blocks for translations

While developing my first more complex blocks I ran into different issues. One of them was extracting the strings for my translations. In my previous blog post I showed you how to get those strings for PHP files. Those methods also work for the JavaScript files as the use “the same functions”, or at least aliases that have the same name.

Translate strings in a Gutenberg block

Let’s start to look at an example of a string translation in a block. This example comes straight from the Create a Block Tutorial and shows an “Edit” component:

import { TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
 
export default function Edit( { attributes, className, setAttributes } ) {
    return (
        <div className={ className }>
            <TextControl
                label={ __( 'Message', 'gutenpride' ) }
                value={ attributes.message }
                onChange={ ( val ) => setAttributes( { message: val } ) }
            />
        </div>
    );
}

The __ function is imported from the @wordpress/i18n component. This code works perfectly fine for translating the string in line 8. It only fails when you try to extract this string into PO file.

Compiled JavaScript file

But why does this example not work when trying to extract the strings? When you work with components like this you usually split up your JavaScript code into multiple files. You then import all those files into a main JavaScript file and then you build the JavaScript file that is used in the browser with something like @wordpress/scripts like explained in the JavaScript Build Setup chapter. This will then create a file build/index.js and the string from the example above will look like this:

// ...
/* harmony import */ var _wordpress_i18n__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @wordpress/i18n */ "@wordpress/i18n");
// ...
function Edit(_ref) {
  var attributes = _ref.attributes,
      className = _ref.className,
      setAttributes = _ref.setAttributes;
  return Object(_wordpress_element__WEBPACK_IMPORTED_MODULE_0__["createElement"])("div", {
    className: className
  }, Object(_wordpress_element__WEBPACK_IMPORTED_MODULE_0__["createElement"])(_wordpress_components__WEBPACK_IMPORTED_MODULE_1__["TextControl"], {
    label: Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_2__["__"])('Message', 'block-i18n'),
    value: attributes.message,
    onChange: function onChange(val) {
      return setAttributes({
        message: val
      });
    }
  }));
}

In line 11 we will find the string again. But it’s not wrapped in a plain __ function. Instead the complied JavaScript files uses some weird function names from the components imported by webpack. As this function/syntax is not known to the wp i18n make-pot command or the X-Poedit-KeywordsList that Poedit is using, the string will not be found when parsing the files.

Solution: Use a different syntax to import components

There is an easy fix for that. You can import components in different ways. Simply replace the import of the i18n functions in line 2 with this “import”:

const { __ } = wp.i18n;

This will then result in the following complied JavaScript file which will use the plain __ function (in line 10) that is known to the parser:

// ...
var __ = wp.i18n.__;
function Edit(_ref) {
  var attributes = _ref.attributes,
      className = _ref.className,
      setAttributes = _ref.setAttributes;
  return Object(_wordpress_element__WEBPACK_IMPORTED_MODULE_0__["createElement"])("div", {
    className: className
  }, Object(_wordpress_element__WEBPACK_IMPORTED_MODULE_0__["createElement"])(_wordpress_components__WEBPACK_IMPORTED_MODULE_1__["TextControl"], {
    label: __('Message', 'block-i18n'),
    value: attributes.message,
    onChange: function onChange(val) {
      return setAttributes({
        message: val
      });
    }
  }));
}

Interestingly, when I removed the import without replacing it, it worked as well. That might be due to the fact that the __ function was already imported by another import and was therefor available. But I would recommend to always define which external components and functions you use in your own JavaScript files. In the same way you can also replace the first line like this:

const { TextControl } = wp.components;

Other tutorials always use this method to import components from WordPress/Gutenberg, so I would highly recommend to do it like this as well. I haven’t had an issue with them and the result complied JavaScript files were even a little smaller in size.

Conclusion

Translating strings in a Gutenberg block (or similar JavaScript code for WordPress) is super easy, as you can simply use the same functions you already know from PHP. But if you do the imports “wrong”, extracting the strings for Poedit does not work.

As I have seen some plugins who do it this way and still have working translations there might be a way to extract them anyways. If you know how they do it, I would highly appreciate if you can leave a comment explaining us, how it works.

Translate a plugin or theme without additional software or plugins

If you want to translate a plugin or theme, I would always recommend to use the free Poedit software which is available on Windows, Linux and Mac. Even though the Pro version has some special WordPress specific feature, you don’t necessarily need it. But I can also highly recommend it. The one-time-payment is quite a good deal and it will save you a lot of time, especially translating themes, which have many similar strings.

But this blog post is not about how to use Poedit to translate plugins and themes. I want to show you how you can translate them using just just commands on a terminal. So even if you can’t translate using additional software (or plugins), it’s possible.

Create a translation template

The first step is creating a POT (Portable Object Template) file with all the strings your plugin/theme is using. When I use Poedit, I usually skip this step and use a language specific template that will allow Poedit to scan for all translatable strings. The easiest way is using a WP-CLI command to create such a file:

wp i18n make-pot . languages/text-domain.pot

When you execute this command, it will scan all PHP and JavaScript files for string with the text domain equal to the plugin/theme folder name and create a POT file in the languages folder (make sure this folder exists). The command knows all the different functions that WordPress is using for translation (the same functions I have in my template above).

Create a locale specific translation

Next you create a translation file for a specific language by copying the POT file into a PO (Portable Object) file. This file has to be prepended with the locale:

cp languages/text-domain.pot languages/text-domain-de_DE.po

You can now open up this file in a text editor and translate the strings manually. In the header of the file you can also define the language but it’s technically not necessary.

Generate the binary translation file

WordPress is using MO (Machine Object) files to load translations. There is also a command to do this task which is available on many operating systems (or included in git bash, cygwin or similar on Windows):

msgfmt languages/text-domain-de_DE.po -o languages/text-domain-de_DE.mo

This is all you need to generate the translations. You can now check them on your WordPress installation (make sure you used the corresponding load_textdomain function to load translations for your text domain in your plugin/theme).

Bonus: Updating translations

Any time you add more strings or you change some of them, you have to update your translation files. When you use software like Poedit (and you have properly configured it), you can simply scan for those new string. When you just use the terminal, you can also achieve this. Just run the command again:

wp i18n make-pot . languages/text-domain.pot

But this will simply just overwrite the template file with a new version. As POT files do not contain any translated string (as the are not for a specific language), that not an issue. But how do you update your localized translation file, without translating all strings again? For this, you can use the merge argument:

wp i18n make-pot . languages/text-domain-de_DE.po --merge=languages/text-domain-de_DE.po

This will scan all PHP and JavaScript files from the current directory, merge in the current translation file and write everything back into this file. Only those strings that are not already in that file will be added. The already translated strings will be left as they are. The only downside: this will not remove any strings that are no longer in your PHP or JavaScript files.

Conslusion

When you really want to easily translate plugins or themes, use Poedit or a similar solution. But if you can’t use them, now you should be able to translate them using some simple commands.

One area, where the wp i18n make-pot command might be very useful is in your CI/CD workflow. You setup a task wich runs the command making sure that the POT file is always up to date with your latest development.

Deactivate blocks for specific post types

In my project I use WordPress as a backend for a native app, I have implemented two custom post types. For booth post types only a limited set of blocks should be available. That’s simply due to the fact, that not all contents can be rendered/used in the app.

Filter allowed blocks

We can use the filter allowed_block_types to allow only a limited set of blocks that can be used or allow/disallow all of them. If you just want to allow some blocks for a specific post type, use the filter like this:

function cpt_allowed_blocks( $allowed_block_types, $post ) {
	if ( 'my_post_type' === $post->post_type ) {
		return array(
			'core/paragraph',
			'core/list',
			'core/quote',
			'core/code',
			'core/table',
			'core/image',
			'core/video',
			'core/audio',
		);
	}

	return $allowed_block_types;

}
add_action( 'allowed_block_types', 'cpt_allowed_blocks', 10, 2 );

In the callback function we first check, if we are currently editing our custom post type. If we do, we return an array with only the block included we want allow in our post type. If not, we return the original value.

This original value can either be an array or a boolean. If you return true, all blocks are allowed. If you return false no blocks are allowed.

Bonus: Set a blocks template for a post type

If you limit the blocks you want to allow, you probably want to ensure a specific layout in your post type. You can help with that by adding some blocks by default, every time an entry for that post type is created. This can be done when registering the post type:

function cpt_register_post_type() {
	register_post_type(
		'my_post_type',
		array(
			'label'                 => __( 'My Post Type', 'text_domain' ),
			'supports'              => array( 'title', 'editor' ),
			'public'                => true,
			'show_ui'               => true,
			// ... other settings
			'has_archive'           => true,
			'show_in_rest'          => true,
			'template'              => array(
				array(
					'core/paragraph',
					array(
						'placeholder' => __( 'Lorem ...', 'text_domain' ),
					),
				),
				array(
					'core/image',
				),
			),
		)
	);
}
add_action( 'init', 'cpt_register_post_type' );

With the template parameter, you can define a number of blocks to include every time a post type entry is created. Each block is represented by an array. For many blocks you can also set options. In this example we add a paragraph with a placeholder text (not content) and an image with no options. This image block will be in it’s “edit state”, just as if you have added it to a post yourself:

Conclusion

When working with custom post types, you can easily limit the blocks that are allowed to be used. Adding a template with some default blocks for new entries really increase the usability of your custom post types a lot.

Change the default email “from name” and “from email”

In WordPress, emails are sent from different places. Most typically are mails sent by a form plugin. In probably most of them you can set the “from name” and “form email”. But there are also mails that are sent from WordPress core. One example of such an email is the “Password Recovery Mail”. Also some other plugins might send emails either to a user or to the admin.

Change the default “from” values

If an email is sent and the name and email address are not set explicitly, WordPress will always use “WordPress” and “[email protected]” as the defaults. Unfortunately there is no setting in the dashboard to change those default.

Overwrite the values using filters

There are two filter you can use to overwrite the name and the email address. Simply put the following into a (mu-)plugin and activate it:

// Change the "from name"
function wp_email_set_from_name( $from_name ) {
	return 'Your Name';
}
add_filter( 'wp_mail_from_name', 'wp_email_set_from_name' );
// Change the "from email"
function wp_email_set_from_email( $from_email ) {
	return '[email protected]';
}
add_filter( 'wp_mail_from', 'wp_email_set_from_email' );

This will overwrite the two values. But it will overwrite it for any email. That’s probably not what you want. Unfortunately the filters are only called, when the default values have already been set. So we cannot simply check if explicit values have been used while sending an email. But we can still check for those default values and only set you own values, if they match the defaults:

// Change the default "from name"
function wp_email_set_from_name( $from_name ) {
	if ( 'WordPress' === $from_name ) {
		return 'Your Name';
	}

	return $from_name;
}
add_filter( 'wp_mail_from_name', 'wp_email_set_from_name' );
// Change the default "from email"
function wp_email_set_from_email( $from_email ) {
	if ( false !== strpos( $from_email, '[email protected]' ) ) {
		return '[email protected]';
	}

	return $from_email;
}
add_filter( 'wp_mail_from', 'wp_email_set_from_email' );

This should now only set new values, if they are the default values. You can still set another name or email address e.g. in a form plugin.

Conclusion

Overwriting the default name and email address for sent emails can be done with some filters. But be careful not to overwrite them for any case, but only in those cases, where the defaults are used, not when another name and email address have been set already.

Create users in WordPress using the REST API

For a current project, we use WordPress as the backend for a native mobile app. The app is developed by another company. I’m responsible for the WordPress part. The content for the app is being created using multiple custom post types and custom taxonomies, as well as some special blocks for a better editing experience. The app can be used without any registration. But some features need a registration, such as synchronizing results on multiple devices or comparing results with other users.

Preparing the server to accept requests

In order to make requests to the server, some response headers have to be set, especially for the iOS version. I first tried to set them using the server configuration, just to realize, that WordPress sets some response headers as well, which resulted in some duplicate headers with the same key, but different values. After some debugging I’ve found some hooks I could use:

function app_rest_headers_allowed_cors_headers( $allow_headers ) {
	return array(
		'Origin',
		'Content-Type',
		'X-Auth-Token',
		'Accept',
		'Authorization',
		'X-Request-With',
		'Access-Control-Request-Method',
		'Access-Control-Request-Headers',
	);
}
add_filter( 'rest_allowed_cors_headers', 'app_rest_headers_allowed_cors_headers' );

For another hook there was no filter, so I had to explicitly call the PHP header() function for this one:

function app_rest_headers_pre_serve_request( $value ) {
	header( 'Access-Control-Allow-Origin: *', true );

	return $value;
}
add_filter( 'rest_pre_serve_request', 'app_rest_headers_pre_serve_request', 11 );

After this small preparation, we can try to create a user with the users endpoint, but this is not going to work. Why not? Well, simply because you cannot create a user for a WordPress site using the REST API without authorization. If that would be possible, that wouldn’t really be good, because otherwise anyone could register a user on an WordPress site.

Authenticate a user with the REST API

So how to we authenticate against the REST API? There are different ways. A very new one is just around the corner and it’s going to be the first one to be integrated into Core: Application Passwords.

There is currently a proposol to integrate the feature project into the upcoming WordPress 5.6. If you want to use it already, you can install the plugin Application Passwords.

Create an app user for the user management

By default, only admin users can manager users. But you probably don’t want to allow an app to have every capability an admin has on a WordPress site. Therefore it’s better to create a special user that only has those capabilitites managing users. You can do that by using a plugin like Members, create a role and a user with PHP code or you use the WP-CLI:

wp role create app App --clone=subscriber
wp cap add app create_users
wp user create app-rest-user [email protected] --role=app

The user needs to have at least the create_users capability to create users and you should also add the read capability, so you can login with the new user and set the application password (this is why we clone the “subscriber” role in the example above). Alternatively you can set the application password for a user with an admin user.

Get the application password for the new user

After you have created the user, login with that user and navigate to the profile settings (or edit it’s profile with an admin user). Here you find a new profile field to create application passwords. Choose a name and hit “Add new”:

This will open a modal with the generated password. You have to copy that password from the modal, as you cannot get it’s plain text after closing the modal.

Now we have everything we need for your API call to create users.

Create your first user

Depending on which system you use to make calls against the WordPress REST API, you workflow looks different. I’ll just use a plain curl call from the terminal to demonstrate the request:

curl --user "app-rest-user:younfj4FX6nnDGuv9EwRkDrK" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"username":"jane","password":"secret","email":"[email protected]"}' \
     http://example.com/wp-json/wp/v2/users

If you have set up everything correct, you should get a JSON response with the data of the new user.

Conclusion

Managing users is possible with some setup work and (currently) some external authentication plugins, but with the new applications password feature it will become a lot easier. In a similar way you can make any other call to the REST API that needs an authorized user. For better security, it’s best to create a specific user for such calls (unless you want to create content assigned to a specific user).

Deactivate individual block styles

In my last blog post I’ve showed you how to deactivate page templates in a child theme and this week I have another tip on how to deactivate some of the things that might come with a parent theme or a plugin. Today I will write about block styles. They are normally defined by Core or the theme, but sometimes they are introduced by plugins.

Deactivate them using a dequeue hook

Block styles are defined using a JavsScript file. That file must be enqueued into the page. This is where we can use a hook to dequeue that file. It could look like this:

function dibs_dequeue_assets() {
	wp_dequeue_script( 'editorskit-editor' );
}
add_action( 'enqueue_block_editor_assets', 'dibs_dequeue_assets', 11 );

This example would remove the main editor JavaScript file, which will not only disable block styles, but all JavaScript code of that plugin. So this is only a good solution, if there is a separate file just with the registrations of block styles.

Deactivate specific block styles

If you just want to remove specific block styles from the backend, you have to use some JavaScript code yourself. So first we have to enqueue a JavaScript file we can then use:

function dibs_enqueue_assets() {
	wp_enqueue_script(
		'dibs-remove-block-styles',
		plugins_url( 'remove-block-styles.js', __FILE__ ),
		array( 'wp-blocks', 'wp-dom-ready', 'wp-edit-post' ),
		filemtime( plugin_dir_path( __FILE__ ) . 'remove-block-styles.js' ),
		true
	);
}
add_action( 'enqueue_block_editor_assets', 'dibs_enqueue_assets', 11 );

The enqueued file need the dependencies wp-blocks, wp-dom-ready and wp-edit-post to work properly. After we have enqueued the file, we can use it to unregister some block styles:

wp.domReady( function() {
	wp.blocks.unregisterBlockStyle(
		'core/image',
		'editorskit-diagonal'
	);
	wp.blocks.unregisterBlockStyle(
		'core/image',
		'editorskit-inverted-diagonal'
	);
	wp.blocks.unregisterBlockStyle(
		'core/cover',
		'editorskit-diagonal'
	);
	wp.blocks.unregisterBlockStyle(
		'core/cover',
		'editorskit-inverted-diagonal'
	);
} );

After the editor has loaded, we will use wp.blocks.unregisterBlockStyle() with the first parameter set to the slug of the block we want to unregister a block style for. The second parameter is the slug of the block style. In this example we would remove the same two block styles from the core/image and from the core/cover block.

This approach is likely the one you want to use, as you don’t want to remove the complete JavaScript as in the first snippet, but only the block styles. And of those you might want to remove only a few.

Conclusion

If a theme or plugin introduces block styles, you don’t want to use, you can use a small JavaScript file to remove them. You just have to be aware, that even if you remove the block styles from being selected for new blocks, the previously selected block styles are still attached to block with the corresponding CSS file. So if you want to remove the block styles from the front end as well, you either have to remove the CSS classes on the old blocks or remove/overwrite the CSS parts in your (child) theme.

Deactivate or replace a page templates in a child theme

This week one of my colleagues was working on a new project, where a page template in the parent theme was broken. There was a better similar page template to use instead, but there might still be the chance that someone would use this broken page template, when creating a new page. So I had the idea to find out, how to deactivate a page template from the dropdown.

Deactivate a page template

In a child theme, it is quite easy to create a new page template or to overwrite a page template, by copying it over from the parent theme. But how can you prevent a page template from showing up in the dropdown? Fortunately, there is almost always a hook for that. If you want to deactivate a page template, use this filter:

function child_theme_disable_page_template( $post_templates, $theme, $post, $post_type ) {
	unset( $post_templates['page-templates/tpl-hero-light.php'] );

	return $post_templates;
}
add_filter( 'theme_templates', 'child_theme_disable_page_template', 10, 4 );

The filter theme_templates will get an array with all templates, using the relative path as the key and the (translated) name as a value. It could look like this:

$post_templates = array(
	'page-templates/tpl-fullscreen-light.php'  => 'Fullscreen, light header',
	'page-templates/tpl-fullscreen.php'        => 'Fullscreen',
	'page-templates/tpl-fullwidth-notitle.php' => 'Full Width, no page title',
	'page-templates/tpl-fullwidth.php'         => 'Full Width',
	'page-templates/tpl-hero-light.php'        => 'Hero, light header',
	'page-templates/tpl-hero.php'              => 'Hero',
);

The array contains page templates from both the parent theme and the child theme. If both have a page template with the same path, only one of it will be included in the array.

Replace a page template

So now you know how to avoid someone from using a page template you don’t want them to use. But what about all the pages that already have that page template selected? You could do a search/replace in the database, replacing all postmeta with the key _wp_page_template and replace it with a new value. But maybe you want to keep the information in the database intakt. Then you can use a variety of hooks to overwrite the path. This is one of the filters you can use:

function child_theme_overwrite_page_template( $template ) {
	if ( get_parent_theme_file_path( '/page-templates/tpl-hero-light.php' ) === $template ) {
		return get_theme_file_path( '/page-templates/tpl-hero.php' );
	}

	return $template;
}

add_filter( 'template_include', 'child_theme_overwrite_page_template' );

In this example, any page that would use the page-templates/tpl-hero-light.php page template from the parent theme will now be using the page-templates/tpl-hero.php page template from the child theme.

Conclusion

While it’s very easy to add or overwrite page templates in a child theme, deactivating one in a child theme is not as easy. But as WordPress usually has a hook for anything you want to customize, even this task is not that hard.