How to modify and patch WordPress core on your live site safely using Composer patches

WordPress.org typically make a major release every 3-4 months. This usually introduces a bunch of useful features but they also fix a lot of bugs and issues people have been experiencing with the previous versions. Most of the time you probably don’t notice these problems as WordPress in general is a stable piece of software.

This can be a problem if there is a bug in WordPress that affects you. Even if the issue has been recently fixed in trunk it will not reach you until the next stable release of WordPress.

This is where Composer patches is extremely useful. It allows you make modifications to WordPress core (or any package you load via Composer) that are only applied after the package has been loaded.

You can safely modify core without forgetting what you have changed or having your changes wiped out after every new release.

A simple guide to modifying WordPress the right way

The first thing you will need to do is install WordPress via Composer. I would recommend using Roots/Bedrock for this. The project is already configured to load WordPress via Composer but also allows you to manage any dependencies (themes/plugins) via Composer too.

To install Bedrock you can open terminal and run the following command.

composer create-project roots/bedrock your-project-folder-name

Navigate to the project you have just created and run the following command to install the composer patches plugin

composer require cweagans/composer-patches

Lets create a folder in the root of our projects to store all of our patches.

mkdir patches

Now we are ready to patch our first bug in WordPress.

There is currently a bug in the Requests library that powers the WP_HTTP class and functions like wp_remote_get(), wp_remote_post(), etc. If you make a request to a resource that uses the referer URL to verify a request then it will not be passed correctly.

To fix this you can comment out line #376 in the /web/wp/wp-includes/Requests/Transport/cURL.php file. It should look like the code snippet below.

curl_setopt($this->handle, CURLOPT_URL, $url);
#curl_setopt($this->handle, CURLOPT_REFERER, $url);
curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
if (!empty($headers)) {
  curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
}

If you run a git diff command on the install of johnpbloch/wordpress it will show you the changes you have made against the master branch.

diff --git a/wp-includes/Requests/Transport/cURL.php b/wp-includes/Requests/Transport/cURL.php
index 4429edb..4f385a9 100644
--- a/wp-includes/Requests/Transport/cURL.php
+++ b/wp-includes/Requests/Transport/cURL.php
@@ -373,7 +373,7 @@ class Requests_Transport_cURL implements Requests_Transport {
                        curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
                }
                curl_setopt($this->handle, CURLOPT_URL, $url);
-               curl_setopt($this->handle, CURLOPT_REFERER, $url);
+               #curl_setopt($this->handle, CURLOPT_REFERER, $url);
                curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
                if (!empty($headers)) {
                        curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);

If you run the command below it will parse the contents of the difference into a patch file that can be applied to our install of WordPress.

I prefer to name my patches with a reference to the ticket id on trac. When a update to WordPress is released I can check to see whether the issue has been resolved and if I can remove the patch.

git diff -p ../patches/37820.patch

The final step is to add the patches section to your composer.json file. This will tell Composer to apply the patch every time WordPress is installed. If Git fails to apply the patch it will fail and throw an error. This will usually happen if this part of the code has changed in a WordPress release.

"extra": {
  "installer-paths": {
    "web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
    "web/app/plugins/{$name}/": ["type:wordpress-plugin"],
    "web/app/themes/{$name}/": ["type:wordpress-theme"]
  },
  "wordpress-install-dir": "web/wp",
  "patches": {
    "johnpbloch/wordpress": {
        "wp_remote_get referrer not being sent correctly": "patches/37820.patch"
    }
  }
},

If you try to run a composer update your terminal should produce a similar output to this.

Gathering patches for root package.
Loading composer repositories with package information
Updating dependencies (including require-dev)
Gathering patches for root package.
Gathering patches for dependencies. This might take a minute.
  - Installing johnpbloch/wordpress (4.6.1)
    Loading from cache

  - Applying patches for johnpbloch/wordpress
    patches/37820.patch (wp_remote_get referrer not being sent correctly)

Generating autoload files

Congratulations. You can now safely make changes to WordPress core in a scalable way. Wherever you deploy your code and run composer install it will run the modified version of WordPress. When you are ready to remove the patch just delete the file and remove the lines from your composer.json file.

Making the WordPress REST API Read Only

If you need to create, update or delete content using the WP REST API then you will need to be authenticated.

However, I have often found myself when building small static sites to only ever need the API to render content on the front end and not change it.

I think it makes sense to remove these requests methods if you do not intend on using them. The code snippet below will remove any non readable (GET) request methods from the /wp/v2/* endpoints.

add_filter( 'rest_endpoints', function( array $endpoints ) {
	foreach ( $endpoints as $endpoint_key => $endpoint_value ) {
		if ( 0 !== strpos( $endpoint_key, '/wp/v2' ) ) {
			continue;
		}

		foreach ( $endpoint_value as $route_key => $route_value ) {
			if ( isset( $route_value['methods'] ) && 'GET' !== $route_value['methods'] ) {
				unset( $endpoints[ $endpoint_key ][ $route_key ] );
			}
		}
	}

	return $endpoints;
});