Améliorer le changelog de votre extension

Améliorer le changelog de votre extension

Je voudrais vous partager un code que j’ai fait pour SecuPress, pour modifier la façon dont le changelog est affiché. Je parle de la boite qui s’ouvre quand on clique sur « Voir les détails de la version x.x » quand une mise à jour est disponible.

Popup du changelog SecuPress

Cet article est donc non seulement destiné aux développeur·e·s mais surtout aux auteur·e·s d’extensions car ce code sera à insérer dans le leur. Aussi le code partagé ici – est GPL – ne fonctionne que pour les extensions hébergées sur le dépôt officiel de WordPress.org et non pour les produits premium. Vous pourrez toujours détourner ce code pour le faire, bien entendu (puisque je le fais pour SecuPress Pro !).

Changer la popup du changelog

D’abord et comme toujours, il va falloir se poser la question « il y a un hook pour ça ? » et souvent la réponse est « oui ». Ensuite c’est « il est où ce hook, comment je le trouve ? ».

Ma façon de faire est d’aller chercher un morceau de ce que je vois à l’écran, du texte, en anglais, si possible quelque chose qui m’assure de le trouver à peu d’endroit, je vais choisir « Requires WordPress Version » . Et comme je suis dans une page d’administration, mise à jour d’extension, je vais chercher ça partout dans le core de WordPress mais uniquement dans le dossier wp-admin.

Parfait, 1 seul résultat, je sais alors que c’est ici que je vais pouvoir intervenir. En remontant un peu dans le code, je vois que je suis dans la fonction install_plugin_information(). Je vais maintenant chercher qui appelle cette fonction et je trouve un hook que WordPress ajoute lui même !

Voilà, on a notre hook, c’est 'install_plugins_pre_plugin-information'. Attention, ce hook est utilisé pour toutes les extensions, donc je vais devoir ajouter des conditions afin d’éviter de modifier le changelog de tout le monde !

Aussi, la fenêtre d’origine charge beaucoup plus de contenu que ce que je veux en faire, on peut normalement changer d’onglet pour passer de « changelog » à « description » etc, mais j’ai décidé que si on clique sur « Voir les détails » alors seul le changelog serait affiché.

Commençons par ajouter un premier hook qui décidera si oui ou non on doit se hooker pour modifier le retour du changelog. Un simple admin_init suffit.

add_action( ‘admin_init’ );

add_action( 'admin_init', 'secupress_hook_changelog' );
function secupress_hook_changelog() {
	if ( isset( $_GET['tab'], $_GET['plugin'], $_GET['section'] )
	&& 'secupress' === $_GET['plugin'] && 'changelog' === $_GET['section'] && 'plugin-information' === $_GET['tab'] ) {
		remove_action( 'install_plugins_pre_plugin-information', 'install_plugin_information' );
		add_action( 'install_plugins_pre_plugin-information', 'secupress_hack_changelog' );
	}
}

On vérifie si on a bien tab, plugin et section dans les paramètres de la requête et pour être dans notre cas quelques conditions, c’est à dire la demande d’information sur une extension sur la section changelog.

Si c’est bien ça, il va falloir supprimer le hook de WordPress pour ajouter le notre, un simple remove_action avant notre add_action.

Maintenant, disons qu’on reçoit la demande, quelqu’un a cliqué, nous allons donc voir notre hook lancé et la callback aussi, voici ce qu’on va faire :

  • Faire un appel à l’API de WordPress
  • Parser le contenu du changelog qui nous intéresse (je ne garde pas tout !)
  • Afficher du CSS maison en header
  • Afficher le contenu de l’API et du contenu personnalisé
  • Afficher des infos relative aux stats de l’extension en footer

plugins_api()

Voici le début de la fonction (je vous la donne en entier à la fin) :

function secupress_hack_changelog() {
	global $admin_body_class;
	$api = plugins_api( 'plugin_information', array(
		'slug' => 'secupress',
		'is_ssl' => is_ssl(),
		'fields' => [
			'short_description' => false,
			'reviews' => false,
			'downloaded' => false,
			'downloadlink' => false,
			'last_updated' => false,
			'added' => false,
			'tags' => false,
			'homepage' => false,
			'donate_link' => false,
			'ratings' => false,
			'active_installs' => true,
			'banners' => true,
			'sections' => true,
		]
	) );

	if ( is_wp_error( $api ) ) {
		wp_die( $api );
	}
…

Je mets en global $admin_body_class vous verrez pourquoi plus tard mais je pense que vous avez déjà compris.

On fait un appel API avec plugins_api() et ses paramètres qui nous intéressent. On vérifie qu’on a pas une erreur en retour sinon on stop avec l’erreur en question.

parsing

…
	$changelog_content = $api->sections['changelog'];
	$changelog_content = explode( "\n", $changelog_content );
	$changelog_content = array_slice( $changelog_content, 0, array_search( '</ul>', $changelog_content ) );
	$changelog_version = strip_tags( array_shift( $changelog_content ) );
	$changelog_content = implode( "\n", $changelog_content );
…

Vient le parsing du changelog dans $changelog_content, ici c’est mon bazar. En fait on réceptionne toute la section changelog qu’on trouve dans le readme.txt que vous avez dans votre version à mettre à jour. De 1, je ne veux pas tout afficher, de 2 je veux l’afficher différemment.

Je commence par en faire un tableau en cassant les lignes, puis je cherche le premier </ul> pour ne garder que ce qui est avant, donc la dernière partie du changelog, la dernière version.

J’alimente aussi une variable $changelog_version que je récupère du contenu. J’aurais pu utiliser $api->version me direz vous ? Oui, mais l’avantage ici c’est que si dans le readme je mets plutôt 1.4.2 – 23 april 2018 je récupère tout d’un coup ! Et enfin je reforme une chaîne avec des retours à la ligne.

HTML/CSS

Allez, on commence le HTML/CSS

…
	iframe_header( __( 'Plugin Installation' ) );
	?>
	<style>
		#plugin-information-title.with-banner div.vignette {
			background-image: url( '<?php echo esc_url( $api->banners['high'] ); ?>' );
			background-size: contain;
		}

		ul {
			list-style: inside;
			padding-left: 15px;
		}

		code {
			background-color: #EEE;
			padding: 2px
		}

		.star-rating {
			display: inline;
		}

		#plugin-information-footer {
			text-align: center;
			line-height: 1.7em;
		}

		.fyi-description {
			display: none;
		}
	</style>
</head>
…

La fonction iframe_header() va générer une entête HTML correcte jusqu’à la balise <head> suivie des hooks de WordPress côté administration.

Maintenant, libre à moi d’y ajouter du CSS maison, soit comme ici inline, soit dans un fichier. Nous ne sommes pas en front, ce n’est pas un chargement inattendu, ce n’est pas un soucis pour moi niveau perf d’ajouter mes règles inline, libre à vous d’utiliser le hook 'admin_print_styles-plugin-install.php' mais attention, vous devrez de nouveau tester que c’est bien pour vous que ce hook est lancé, sinon votre feuille de styles sera ajoutée pour toutes les extensions ! Je reviens sur ces règles plus tard.

body

…
<body class="$admin_body_class">

<header id="plugin-information-title" class="with-banner">
	<div class="vignette"></div>
	<h2>SecuPress Free <?php echo esc_html( $changelog_version ); ?></h2>
</header>
…

Nous voici dans le <body>et voilà la fameuse classe CSS globale qu’on ajoute à notre balise. Puisque les CSS de WordPress sont aussi chargées, il faut que notre balise body soit correctement imprimée.

J’ajoute une balise <header>avec un ID spécifique à WordPress, merci la balise body et sa classe CSS, et aussi une classe CSS. Je pars du principe que j’ai une bannière, si vous voulez faire des tests conditionnels, allez-y.

La balise <div class="vignette"></div> permet de créer ce petit effet autour de notre bannière et aussi d’ajouter via nos règles CSS en background notre bannière venant directement de WordPress, voyez :

#plugin-information-title.with-banner div.vignette {
			background-image: url( '<?php echo esc_url( $api->banners['high'] ); ?>' );
			background-size: contain;
		}

C’est ici que je la récupère, même histoire, je pars du principe que j’ai une bannière HD (high) et pas seulement les anciennes (low).

Puis j’affiche mon fameux titre dans une <h2> qui peut contenir une date ou tout autre information. On continue avec le changelog lui même.

section

…
<section id="plugin-information-scrollable">
	<?php
	$changelog_content = wp_kses( $changelog_content, ['code' => ['id'=>1, 'class'=>1], 'ul' => ['id'=>1, 'class'=>1], 'li' => ['id'=>1, 'class'=>1] ] );
	echo $changelog_content;
	?>
	<p><a href="https://secupress.me/pricing/" class="button">SecuPress Pro</a></p>
	<p><em><?php _e( 'Read <a target="_blank" href="https://secupress.me/changelog">full changelog</a> on SecuPress.me', 'secupress' ); ?></em></p>
</section>
…

Une balise section avec un ID qui permet d’avoir une boite scrollable.

Je sanitize (désinfecte) avec wp_kses() et ses quelques balises autorisées. J’aurais pû le faire avant mais je suis adepte du « Escape Late » donc je fais le travail d’échappement et désinfection le plus tard possible.

N’oublions pas que ça reste un contenu qui est écrit par un humain, je me dois de le sécuriser avant de l’afficher, contrairement à l’API qui vient de WordPress lui même.

Ne me sortez pas l’argument « et si l’API se fait hacker ? », si elle se fait hacker, les hackers vous envoient un zip corrompu à la place du vrai, site hors service… On continue.

Ici je suis libre d’ajouter tout contenu maison, j’y ai personnellement posé un lien vers le changelog plus complet sur le site de l’extension, et j’y ajoute si je veux des Call To Action (CTA) pour passer à la version Pro.

Vient le footer dans lequel on peut y mettre tout ce qu’on veut, donc vous êtes encore libre, j’y ai mis les version requises de WP et PHP, la compatibilité WP, le nombre d’installations active et la note en étoiles.

footer

…
<div id="plugin-information-footer">
	<strong><?php _e( 'Requires WordPress Version:' ); ?></strong>
	<?php
	printf( __( '%s or higher' ), $api->requires );

	if ( ! empty( $api->requires_php ) ) {
		echo '& PHP ' . printf( __( '%s or higher' ), $api->requires );
	}
	?> |
	<strong><?php _e( 'Compatible up to:' ); ?></strong>
	<?php echo $api->tested; ?>
	<br>
	<strong><?php _e( 'Active Installations:' ); ?></strong>
	<?php
	if ( $api->active_installs >= 1000000 ) {
		_ex( '1+ Million', 'Active plugin installations' );
	} elseif ( 0 == $api->active_installs ) {
		_ex( 'Less Than 10', 'Active plugin installations' );
	} else {
		echo number_format_i18n( $api->active_installs ) . '+';
	}
	?> |
	<strong><?php _e( 'Average Rating' ); ?>:</strong>
	<?php wp_star_rating( [ 'type' => 'percent', 'rating' => $api->rating, 'number' => $api->num_ratings ] ); ?>
	<p aria-hidden="true" class="fyi-description"><?php printf( _n( '(based on %s rating)', '(based on %s ratings)', $api->num_ratings ), number_format_i18n( $api->num_ratings ) ); ?></p>
	<br>
</div>
…

J’utilise l’ID #plugin-information-footerqui va lui même créer en CSS la démarcation et le fond gris. Remarquez que je ne crée aucune chaîne à traduire, je ne fais que utiliser ce que WordPress a déjà en stock, ça m’évite une traduction et je suis instantanément compatible avec toutes les langues de WordPress !

Je vous laisse regarder chaque élément, il viennent pour la plupart du core de WordPress, là où j’avais cherché au tout début un morceau de chaîne en anglais, plagiat total quoi.

exit

Et on clos tout ça avec un simple :

…
iframe_footer();
exit;
}

Ceci va fermer les balises body et html, et surtout on sort du script avec exit sinon WordPress continue d’afficher la page d’installation des extensions !

Voici ce que ça donne :

Popup du changelog SecuPress après le dev

Vous êtes vraiment libre, vous pouvez faire mieux !

D’autres règles CSS et autre 

Ajouter des notes de mises à jour

Et si on allait un peu plus loin avec ce changelog et qu’on affichait AVANT que la personne ne mette à jour la version, des informations importantes, comme par exemple la version requise de WordPress et PHP qui auraient pu changer !

C’est aussi ce que je fais pour SecuPress, récemment en 1.4 les versions requises sont passées de WP 3.7 à WP 4.0 et PHP 5.3 à PHP 5.4. Si il faut se mettre à jour pour découvrir ça, aie …

J’ai donc inclus en amont une portion de code qui est capable de faire ceci :

Message d’avertissement de SecuPress Pro

On voit donc que je suis en version 1.4.1 et qu’une version 1.4.2 est disponible, elle m’affiche avant même d’être installée cette information ! Le texte vient bien du readme.txt de la v1.4.2 !

On fait la même chose, on cherche à quel endroit on se trouve dans le core avec « There is a new version of » encore dans le dossier wp-admin.

Dans le fichier updates.php

J’ai un peu plus de résultats, 9 dans 2 fichiers. themes.php ou updates.php, je vois même 'update_plugins' sur la capture, on est bon rapidement quand même ! Nous sommes dans la fonction wp_plugin_update_row().

Voyons si dedans on aurait un hook dispo pour modifier (filtre) ou ajouter (action) du contenu …

in_plugin_update_message-$file

Voilà on a notre hook (dynamique au passage) "in_plugin_update_message-$file", son nom est super clair non ? Pour mon exemple ça sera donc 'in_plugin_update_message-' . SECUPRESS_FILEqui équivaut en fait à 'in_plugin_update_message-secupress/secupress.php'c’est à dire le retour de la fonction plugin_basename() ou plus simplement le dossier de votre extension suivi du fichier principal de celle ci.

Il s’agit donc d’un hook type action, nous allons afficher ce qui nous semble bien. Les étapes :

  • Aller récupérer dans la base de données la version qui doit être mise à jour
  • Allez récupérer les informations de mise à jour (Upgrade Notices)
  • Parser ce contenu et l’afficher

new_version

Encore une fois, je vous donne la fonction par morceaux et le tout à la fin.

add_action( 'in_plugin_update_message-secupress/secupress.php', 'secupress_updates_message', 10, 2 );
function secupress_updates_message( $plugin_data, $new_plugin_data ) {
	// Get next version.
	if ( isset( $new_plugin_data->new_version ) ) {
		$remote_version = $new_plugin_data->new_version;
	}

	if ( ! isset( $remote_version ) ) {
		return;
	}
…

Ce hook nous donne 2 arguments, celui de l’extension installée et quelques informations sur celle qui sera mise à jour.

Il nous suffit de regarder si pour notre extension on a bien une version prête à être mise à jour ou pas, on peut alors directement lire cette version, s’il n’y en a pas, on ne fait rien.

wp_remote_get()

…
	$body = get_transient( 'secupress_updates_message' );

	if ( ! isset( $body[ $remote_version ] ) ) {
		$url = 'https://plugins.svn.wordpress.org/secupress/trunk/readme.txt';
		$response = wp_remote_get( $url );

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return;
		}

		$body = wp_remote_retrieve_body( $response );

		set_transient( 'secupress_updates_message' , array( $remote_version => $body ) );
	} else {
		$body = $body[ $remote_version ];
	}
…

Je vais commencer par aller lire en base si je n’ai pas déjà mis en cache cette information via les transients. Comme ça si je l’ai déjà demandée, pas besoin de faire un nouvel appel pour cette version, le contenu ne peut pas changer.

Ici on ne peut pas utiliser l’API plugins car elle fait son propre parsing et ne va pas nous retourner nos informations. Je vous laisse ouvrir ce fichier readme qui est donc le readme de mon extension sur le dépôt de WordPress.

Voyez vers la fin il y a une section « Upgrade Notice » qui contient une sorte de mini changelog, et bien WordPress ne va pas la garder mais j’en veux ! Je fais alors simplement mon propre appel.

Je vérifie toujours si on a une erreur ou pas sinon on ne fait rien. Dans le cas contraire je récupère le contenu renvoyé, donc TOUT le fichier readme, il va falloir parser !

preg_match()

…
	$regexp = '#== Upgrade Notice ==.*= ' . preg_quote( $remote_version ) . ' =(.*)=#Us';

	if ( preg_match( $regexp, $body, $matches ) ) {

		$notes = (array) preg_split( '#[\r\n]+#', trim( $matches[1] ) );
		$date  = str_replace( '* ', '', wp_kses_post( array_shift( $notes ) ) );
…

Hop, je fais une expression régulière qui me récupère ce qu’il y a entre le numéro de ma version et le prochain, dans la section « Upgrade Notice« . J’en retire les notes de mises à jour et une date de mise à jour. Un peu de preg_split() et de str_replace() pour être propre.

echo

…
		echo '<div>';
		/** Translators: %1$s is the version number, %2$s is a date. */
		echo '<strong>' . sprintf( __( 'Please read these special notes for this update, version %1$s - %2$s', 'secupress' ), $remote_version, $date ) . '</strong>';
		echo '<ul style="list-style:square;margin-left:20px;line-height:1em">';
		foreach ( $notes as $note ) {
			echo '<li>' . str_replace( '* ', '', wp_kses_post( $note ) ) . '</li>';
		}
		echo '</ul>';
		echo '</div>';
	}
}

Et on affiche le tout avec cette fois ci une traduction maison, vous pouvez tout aussi bien ne rien mettre, seules les lignes des notes seront affichées, à vous de voir. Remarquez, toujours un sanitize avec kses avant d’afficher les lignes.

Le code complet

Vous souhaitez l’utiliser dans votre extension ? Postez moi une capture sur twitter !

Lire la suite

Vous aimez ? Partagez !

Consultant en Sécurité, Expert WordPress, Formateur, Marketeur et créateur du plugin de sécurité WordPress SecuPress.
Julio développe et sécurise du contenu web tous les jours. La création de plugins WordPress et la vente de produits WordPress font partie de son quotidien.


Réagir à cet article

120 caractères maximum