Providing custom plugins outside of WordPress.org also means to handle things like plugin information manually. That’s why I searched for a more automated solution for the changelogs. That means that I don’t have to ship it as part of the plugin itself, but to retrieve it automatically from somewhere else.

This approach could be extended to support all kind of plugin information when visiting the “View details” link on the plugin list page in the backend. In this post, I focus solely on retrieving changelogs.

Prerequisite: the update server

In its previous state, I already had some plugin information available such as version, author, supported WordPress and PHP versions and so on. Content-wise however, it was all hardcoded in the plugin. That also meant that e.g. version 1.1.0 of the plugin would always just show the content of this exact version. Whenever an update would be available, it would still not show its content. This is especially important for the changelog, as this should always contain the latest data so that you can check which changes have been made before actually updating and without visiting a remote changelog page on the developer’s site.

For this general information, I’ve implemented the WP Update Server from Yahnis Elsts within a fork containing some specific changes for my setup.

So basically I already have a method to retrieve data from the update server in my plugin. After retrieving that, I add the changelog manually to this dataset in the plugin itself.

Single source of truth

Since this means that, besides of the downside of not having the most up-to-date changelog in older plugin versions, I have to maintain the changelog in the plugin itself as well as in the plugin’s documentation on my website, I wanted to change that.

My original thoughts on this would be a text-based version where additional text files are laying somewhere in the update server and will then be sent within the plugin information. While this would be a solution for outdated changelogs in older plugin versions, I would still have to maintain them in the update server as well on my website.

Thus, I changed the approach to actually load the content of my website in the update server and send this data back to the plugin, where it then can be displayed in the plugin information’s changelog tab.

Server-side implementation

The biggest part was now to do this implementation on the server-side. It needs to download the page – while using the correct locale – and extracting the changelog from it. Luckily, since the plugin information support at least a specific amount of HTML tags, there’s no need to adjust the extracted HTML that much, and the rendering is still pretty neat.

To extract only the changelog, I used a group block to group all of the changelog entries in the changelog area (since the page contains also other documentation). Then, I added an anchor to this group so that I could explicitly grab this group later on with PHP’s DOMDocument.

Since I publish this data in German and English, I’ve added a check for German language (and also support Swiss and Austrian WordPress instances). The locale of the requesting website will already be submitted. Then, I either use the German version for these specific or the English version as default/fallback for all other locales.

The whole code looks like this:

/**
 * Get the changelog.
 * 
 * @param	array	$meta Current metadata
 * @param	string	$locale The specified locale
 * @return	array Updated metadata
 */
private function get_changelog( array $meta, string $locale ): array {
	$urls = [
		'impressum' => [
			'default' => 'https://impressum.plus/en/documentation/',
			'german' => 'https://impressum.plus/dokumentation/',
		],
		'form-block-pro' => [
			'default' => 'https://formblock.pro/en/documentation/',
			'german' => 'https://formblock.pro/dokumentation/',
		],
	];

	if ( empty( $urls[ $meta['slug'] ] ) ) {
		$meta['sections'] = [
			'changelog' => '',
		];

		return $meta;
	}

	switch ( $locale ) {
		case 'de_AT':
		case 'de_DE':
		case 'de_DE_formal':
		case 'de_CH':
		case 'de_CH_formal':
			$url = $urls[ $meta['slug'] ]['german'];
			break;
		default:
			$url = $urls[ $meta['slug'] ]['default'];
			break;
	}

	$html = $this->get_single_api_data( $url );
	$use_internal_errors = \libxml_use_internal_errors( true );
	$changelog = '';
	$dom = new DOMDocument();
	$dom->loadHTML( $html, \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD );
	$group = $dom->getElementById( 'changelog-group' );

	if ( $group ) {
		/** @var \DOMNode $childNode */
		foreach ( $group->firstElementChild->childNodes as $childNode ) {
			if ( \method_exists( $childNode, 'removeAttribute' ) ) {
				$childNode->removeAttribute( 'id' );
			}

			$changelog .= $dom->saveHTML( $childNode );
		}
	}

	\libxml_use_internal_errors( $use_internal_errors );

	$meta['sections'] = [
		'changelog' => \trim( $changelog ),
	];

	return $meta;
}
Code language: PHP (php)

I have a list with all URLs, divided by plugin that request the update server, and further divided by languages. Then, the above mentioned locale checks takes place and selects the correct URL, which will then be downloaded via another method get_single_api_data, which I’ve also added to my custom update server class to retrieve remote data via cURL.

Then, it comes to loading and extracting the changelog part in DOMDocument by its ID. Since I don’t want to include the group <div> itself, I iterate through all child nodes of this group and just save them, before returning (in fact I use the children of the first element child of the group, since in my setup all groups still have an inner container as pre WordPress 5.8).

It also removes all id attributes from the children. Just to make sure there’s no interference here.

This whole method is called within the filterMetadata method of my custom update server and then sent with the other metadata to the plugin.

Adjusting the plugin

Since I already displayed changelogs, I already had a method that uses the plugins_api_result filter to set the metadata accordingly to get them properly displayed when clicking the “View information” link on the plugin overview.

So, instead of adding the static changelog from within the plugin to the changelog section of the metadata, I’ve made a request to the update server to retrieve the metadata and returned them. You will get a JSON code from the update server, make sure to first use json_decode as an associative array and then cast this array to an object to have an object containing strings or arrays, as this is the expected format for WordPress here.

The whole code can then be looking like this:

/**
 * Get some plugin information.
 * 
 * @param	object	$res The plugin information resource
 * @param	string	$action The current action
 * @param	object	$args Additional plugin arguments
 * @return	object The updated plugin information resource 
 */
function my_get_plugin_information( object $res, string $action, array $args ): object {
	if ( $action !== 'plugin_information' ) {
		return $res;
	}
	
	if ( empty( $args->slug ) || $args->slug !== IMPRESSUM_BASE ) {
		return $res;
	}
	
	$request = \wp_remote_post( $url, $metadata_args );
	
	if ( \is_wp_error( $request ) ) {
		return $res;
	}
	
	$response = \wp_remote_retrieve_body( $request );
	$update_server_res = (object) \json_decode( $response, true );
	
	return $update_server_res;
}

\add_filter( 'plugins_api_result', 'my_get_plugin_information', 10, 3 );
Code language: PHP (php)

You need to adjust at least the $url and $metadata_args argument in the request on line 18.

Conclusion

Now every time someone displays the plugin information to view the changelog, the WordPress instance requests the update server for metadata, which then requests the documentation to retrieve the changelog. Everything I need to do is updating the changelog within the documentation to keep everything up-to-date.

Leave a Reply

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