GitHub Action workflow for running PHP_CodeSniffer on pull requests with check annotations

? Idea

For GitHub pull requests, detect any coding standard violations via PHP_CodeSniffer. Errors should prevent the pull request from being merged and displayed to the user.

? Implementation

Example of a check annotation
Example of a job output

? Workflow file

YAML / RAW / github:gist
name: PHP_CodeSniffer

on: pull_request

jobs:
  phpcs:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '7.3'
          coverage: none
          tools: composer, cs2pr

      - name: Get Composer cache directory
        id: composer-cache
        run: echo "::set-output name=dir::$(composer config cache-files-dir)"

      - name: Setup cache
        uses: pat-s/always-upload-cache@v1.1.4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          # Use the hash of composer.json as the key for your cache if you do not commit composer.lock. 
          # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --prefer-dist --no-suggest --no-progress

      - name: Detect coding standard violations
        run: vendor/bin/phpcs -q --report=checkstyle | cs2pr --graceful-warnings

The workflow file can be saved in the .github/workflows directory of your GitHub repository.

Notes:

  • The workflow is using the pat-s/always-upload-cache action, a fork of actions/cache, which also supports caching if a previous step fails.
  • Your repository should include a configuration file for PHP_CodeSniffer.
  • If the coding standard isn’t one of the defaults it need to be added as (dev) dependency in your project’s composer.json file.
  • Only 10 warning and 10 error annotations per step are currently supported (source). It’s recommended to fix all existing errors before publishing the workflow.
  • To exit with error codes if there are only warnings you can remove the --graceful-warnings flag in the last line.

Questions? Feedback? Let me know in the comments!

Photo by Alexander Sinn on Unsplash

Benutzerdefinierte Feeds in WordPress anlegen

WordPress bietet standardmäßig RSS Feeds für Beiträge, Kommentare, Taxonomien, Archive und Custom Post Types an. In „Alle Feeds einer WordPress-Installation“ hat Vladimir diese mal aufgelistet.

Die Inhalte der vorhandenen Feeds können mit den richtigen Filtern angepasst werden. Doch wie kann ein eigener Feed angelegt werden?

Mit add_feed() einen eigenen Feed anlegen

Die seit WordPress 2.1.0 mitgelieferte Funktion add_feed() befindet sich in wp-includes/rewrite.php und ist, wie der Name schon verspricht, für das Anlegen neuer Feeds zuständig. Die Funktion ist folgendermaßen aufgebaut:

/**
 * Add a new feed type like /atom1/.
 *
 * @since 2.1.0
 *
 * @param string $feedname
 * @param callback $function Callback to run on feed display.
 * @return string Feed action name.
 */
function add_feed($feedname, $function) {
	global $wp_rewrite;
	if ( ! in_array($feedname, $wp_rewrite->feeds) ) //override the file if it is
		$wp_rewrite->feeds[] = $feedname;
	$hook = 'do_feed_' . $feedname;
	// Remove default function hook
	remove_action($hook, $hook, 10, 1);
	add_action($hook, $function, 10, 1);
	return $hook;
}

Zunächst erwartet die Funktion einen Slug für den Feed. Es ist der Name, unter welchem der Feed später aufrufbar sein wird. Er sollte deswegen nur aus den Zeichensatz [a-z0-9_-] bestehen. Die Feed URL wird bei aktivierten Permalinks im Format example.com/feed/{feed_slug} sein.
Der zweite Teil ist die Callback-Funktion. Diese Funktion sollte den Feed rendern und wird beim Aufruf des Feeds aufgegriffen.

Das feed-rss2.php Template

feed-rss2.php ist ein Template in WordPress und übernimmt die Darstellung für jegliche Art von Post Type im RSS2 Format.

Das Template ist somit die optimale Ausgangslage für die Darstellung des eigenen Feeds und kann deswegen in der Callback-Funktion geladen werden.

Anregung für PHP 5.3:

function ds_custom_feed() {
	add_feed( 'custom', function() {
		load_template( ABSPATH . WPINC . '/feed-rss2.php' );
	} );
}
add_action( 'init', 'ds_custom_feed' );

Feed mit eigenen Inhalt füllen

Was nun noch fehlt, ist der Inhalt des Feeds. Dazu als Beispiel folgende Klasse anschauen:

PHP / RAW / github:gist
<?php
/**
 * Add a custom feed to WordPress.
 *
 * The feed will be rendered through the wp-includes/feed-rss2.php template
 * and avaiable under example.com/feed/{$feed_slug}.
 *
 * Note: Don't forget to flush the rewrite rules once.
 *
 * @author Dominik Schilling
 * @link   https://dominikschilling.de/897
 */
class DS_Custom_Feed {
	/**
	 * Sets the feed slug.
	 *
	 * @var string
	 */
	public static $feed_slug = 'custom';

	/**
	 * Registers the feed and the pre_get_posts action
	 */
	public static function init() {
		add_feed( self::$feed_slug, array( __CLASS__, 'feed_template' ) );

		add_action( 'pre_get_posts', array( __CLASS__, 'feed_content' ) );
	}

	/**
	 * Customizes the query.
	 * It will bail if $query is not an object, filters are suppressed and it's not
	 * our feed query.
	 *
	 * @param  WP_Query $query The current query
	 */
	public static function feed_content( $query ) {
		// Bail if $query is not an object or of incorrect class
		if ( ! $query instanceof WP_Query )
			return;

		// Bail if filters are suppressed on this query
		if ( $query->get( 'suppress_filters' ) )
			return;

		// Bail if it's not our feed
		if ( ! $query->is_feed( self::$feed_slug ) )
			return;

		// Change the feed content
		// Example: A feed for pages
		$query->set( 'post_type', array( 'page' ) );
	}

	/**
	 * Loads the feed template which is placed in wp-includes/feed-rss2.php.
	 */
	public static function feed_template() {
		load_template( ABSPATH . WPINC . '/feed-rss2.php' );
	}
}

/**
 * Hooks into `init`.
 *
 * Note: add_feed() needs access to the global $wp_rewrite
 */
add_action( 'init', array( 'DS_Custom_Feed', 'init' ) );

Es wird Gebrauch vom pre_get_posts Hook gemacht. Dieser erlaubt es die Query nach Bedarf anzupassen – hier wird z.B. der Post Type auf page gesetzt. Was alles angepasst werden kann, kann dem Eintrag im Codex entnommen werden.

Als Beispiel kann ich auf meinen Feed für den WPD Planet hinweisen, welcher nur Beiträge mit einem bestimmten Custom Field anzeigt – Nachzulesen im GitHub Repo zum Theme.

Zum Thema

Cache-Busting durch Versionierung im Dateinamen

Von Haus aus versioniert WordPress eingebundene Skripte und Styles. So sieht der Output für die jQuery Bibliothek beispielhaft folgendermaßen aus:
http://example.com/wp-includes/js/jquery/jquery.js?ver=1.8.3

Wofür die Versionierung? Statische Dateien, die eine hohe Auslaufzeit (Expires-Header) haben, werden vom Browser nicht mehr vom Server geladen, sondern direkt aus dem Browsercache eingebunden. Wird die Resource aber auf dem Server geändert, bekommt der Browser dies nicht mit und lädt weiterhin die alte Version aus dem Browsercache. Abhilfe schafft die Versionierung.

WordPress hängt an die Resource eine Query String mit dem Parameter ver. Diese Art von Versionierung wird allerdings nicht empfohlen.
Google schreibt dazu:

Most proxies, most notably Squid up through version 3.0, do not cache resources with a “?” in their URL even if a Cache-control: public header is present in the response. To enable proxy caching for these resources, remove query strings from references to static resources, and instead encode the parameters into the file names themselves.

Die Version soll also mit in den Dateinamen gepackt werden.

Plugin: Dateinamen um die Version erweitern

Um die Dateien nicht physisch zu ändern hilft das mod_rewrite Modul und folgendes Plugin, welches sich in die WordPress Filter script_loader_src und style_loader_src einklinkt.

PHP / RAW / github:gist
<?php
/**
 * Plugin Name: Filename-based cache busting
 * Version: 0.3
 * Description: Filename-based cache busting for WordPress scripts/styles.
 * Author: Dominik Schilling
 * Author URI: http://wphelper.de/
 * Plugin URI: https://dominikschilling.de/880/
 *
 * License: GPLv2 or later
 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
 *
 *
 * Extend your .htaccess file with these lines:
 *
 *   <IfModule mod_rewrite.c>
 *     RewriteEngine On
 *     RewriteBase /
 *
 *     RewriteCond %{REQUEST_FILENAME} !-f
 *     RewriteCond %{REQUEST_FILENAME} !-d
 *     RewriteRule ^(.+)\.(.+)\.(js|css)$ $1.$3 [L]
 *   </IfModule>
 */

/**
 * Moves the `ver` query string of the source into
 * the filename. Doesn't change admin scripts/styles and sources
 * with more than the `ver` arg.
 *
 * @param  string $src The original source.
 * @return string
 */
function ds_filename_based_cache_busting( $src ) {
	// Don't touch admin scripts.
	if ( is_admin() ) {
		return $src;
	}

	$_src = $src;
	if ( '//' === substr( $_src, 0, 2 ) ) {
		$_src = 'http:' . $_src;
	}

	$_src = parse_url( $_src );

	// Give up if malformed URL.
	if ( false === $_src ) {
		return $src;
	}

	// Check if it's a local URL.
	$wp = parse_url( home_url() );
	if ( isset( $_src['host'] ) && $_src['host'] !== $wp['host'] ) {
		return $src;
	}

	return preg_replace(
		'/\.(js|css)\?ver=(.+)$/',
		'.$2.$1',
		$src
	);
}
add_filter( 'script_loader_src', 'ds_filename_based_cache_busting' );
add_filter( 'style_loader_src', 'ds_filename_based_cache_busting' );

Wie im Plugin Header dokumentiert, muss die .htaccess ergänzt werden.
Wer auf nginx setzt kann folgende Zeilen nutzen, danke Sergej:

location ~ ^(.+)\.(.+)\.(js|css)$ {
    alias $1.$3;
}

Zum Thema

WebKit XSS Filter blockt Inlineframes und Flash Objekte in der WordPress Artikelvorschau

Cross-Site-Scripting (XSS), also das Einfügen von schädlichen Skripter einer Seite A in Seite B, ist ein Sicherheitsfaktor, den es gilt zu unterbinden. Primär sind dafür die Entwickler der jeweiligen Webseite zuständig.
Doch auch heutige Browser klinken sich in die Thematik ein.

Seit geraumer Zeit besitzen WebKit Browser, siehe Chrome und Safari, einen XSS Filter, der überprüft, ob ein Skript, welches auf einer Webseite ausgeführt werden soll, sich auch schon in der POST Anfrage befand. Ist dem so, kann dies ein Indiz für eine XSS-Attacke sein und wird geblockt.

Mit folgenden Beispiel kann der XSS Filter selbst getestet werden:

<?php
if( ! $_POST ) :
?>
<form method="post">
	<input name="xss" value="<iframe src='http://example.com'></iframe>" />
	<input type="submit" />
</form>
<?php
else :
	echo $_POST&#91;'xss'&#93;;
endif;&#91;/code&#93;

<h4>Was hat das nun mit der Artikelvorschau in WordPress zu tun?</h4>
Ganz einfach: Starte Chrome oder Safari, erstelle einen Artikel mit einem YouTube Video und klicke auf den Button <em>Vorschau</em>.

Das Ergebnis:

[caption id="attachment_980" align="aligncenter" width="700"]<img src="https://dominikschilling.de/wp-content/uploads/2012/09/wordpress-preview-webkit.png" alt="WordPress Vorschau im Webkit Browser" title="Artikel Vorschau eines YouTube Videos im Webkit Browser" width="700" height="472" class="size-full wp-image-980" /> Artikel Vorschau eines YouTube Videos im Webkit Browser[/caption]

Man sucht vergeblich das YouTube Video, die Konsole wirft ein <span style="color:red">Refused to execute a JavaScript script. Source code of script found within request.</span> aus und nach einem Reload des Fensters ist das YouTube Video wieder da.

[caption id="attachment_982" align="alignright" width="256"]<img src="https://dominikschilling.de/wp-content/uploads/2012/09/wordpress-aktikelvorschau-weg.png" alt="WordPress Artikelvorschau" title="WordPress Artikelvorschau" width="256" height="388" class="size-full wp-image-982" /> Visualisierung der Artikelvorschau[/caption]

Warum? Der XSS-Filter hat zugeschlagen.

Beim Klick auf den <em>Vorschau</em> Button wird eine POST Abfrage an <code>/wp-admin/post.php</code> gestartet und intern als <code>post.php?action=preview</code> aufgenommen.

Dadurch wird erreicht, dass eventuelle Änderungen in einem Entwurf oder einer Revision (Autosave) gespeichert werden. Genaueres kann man der Funktion <code>post_preview()</code> entnehmen.

Das Wichtige an der Funktion ist die Rückgabe. Er enthält die Weiterleitung zur eigentlichen Vorschau.

Wie bekannt ist, zeigt die Vorschau eine 1:1 Kopie des Artikels samt Änderungen. Dazu zählt auch in unserem Fall der vorher per POST gesendete Artikelinhalt mit dem YouTube Video.
Wir erinnern uns zurück: Der XSS-Filter schlägt zu, wenn ein Skript in einer POST Abfrage gefunden wird und gleichzeitig vom Browser gerendert werden soll.
Trotz dem Umweges ist dies nun bei WordPress genau der Fall.

Und jetzt?

<h4>Den WebKit XSS Filter umgehen</h4>

Mit Hilfe eines nicht-standardisierten HTTP-Headerfeldes <code>X-XSS-Protection</code> kann dem Browser mitteilt werden, dass schon auf der Client-Seite für die nötige Sicherheit gesorgt wurde. Der Wert des Feldes sollte dann mit <code>0</code> belegt werden.

Folgende Zeilen setzen das Vorhaben für unsere Artikelvorschau um und sollte in einem Plugin Platz finden.

[code gist="plugin.php"]

Führt man das obige Szenarion nun erneut aus, so sollte das YouTube Video direkt angezeigt werden.

Zum Thema

jQuery im Footer von WordPress laden — Die elegantere Variante

Seit WordPress 3.9 funktioniert diese Variante leider nicht mehr.

WordPress wird mit einigen JavaScript Bibliotheken ausgeliefert. Dazu gehören zum Beispiel script.aculo.us oder jQuery.
Plugins und Themes müssen diese Bibliotheken dann nicht mehr selbst mitausliefern, sondern können diese über die WP_Scripts API einbinden.

Direkt zur Lösung.

Registierung der Standardbibliotheken

Zuständig für die Registrierung der Standardbibliotheken ist die Funktion wp_default_scripts() in /wp-includes/script-loader.php. Ein Auszug:

<?php
/**
 * Register all WordPress scripts.
 *
 * @since 2.6.0
 *
 * @param object $scripts WP_Scripts object.
 */
function wp_default_scripts( &$scripts ) {
// &#91;…&#93;
	$scripts->add( 'colorpicker', "/wp-includes/js/colorpicker$suffix.js", array('prototype'), '3517m' );

	$scripts->add( 'editor', "/wp-admin/js/editor$suffix.js", array('utils','jquery'), false, 1 );
// […]
	// not used in core, replaced by Jcrop.js
	$scripts->add( 'cropper', '/wp-includes/js/crop/cropper.js', array('scriptaculous-dragdrop') );

	$scripts->add( 'jquery', '/wp-includes/js/jquery/jquery.js', array(), '1.7.2' );

	// full jQuery UI
	$scripts->add( 'jquery-ui-core', '/wp-includes/js/jquery/ui/jquery.ui.core.min.js', array('jquery'), '1.8.20', 1 );
	$scripts->add( 'jquery-effects-core', '/wp-includes/js/jquery/ui/jquery.effects.core.min.js', array('jquery'), '1.8.20', 1 );
// […]
}

WP_Dependencies->add() erwartet also die Argumente in der Reihenfolge Skriptname, Pfad zum Skript, Abhängigkeit zu anderen Skripten, Version des Skripts und zum Schluss das entscheidene Argument Einreihung in den Header oder Footer.

Nach dem selben Prinzip arbeiten übrigens auch wp_register_script() und wp_enqueue_script().

Warum JavaScript eigentlich in den Footer gehört

With scripts, progressive rendering is blocked for all content below the script. Moving scripts as low in the page as possible means there’s more content above the script that is rendered sooner.

The second problem caused by scripts is blocking parallel downloads.

Steve Souders in „High Performance Web Sites: Rule 6 – Move Scripts to the Bottom

Zusammengefasst: Erkennt der Browser ein Skript, wird das Skript zunächst abgearbeitet und alles andere muss warten.

jQuery für den Footer registrieren

Schaut man sich nun nochmal obige markierte Zeile an, so erkennt man schnell, dass der letzte Parameter bei der jQuery Registrierung nicht gesetzt wurde.
Daraus folgt, dass, wenn jQuery eingebunden wird, die Skripteinbindung automatisch in den Header gelegt wird.

Gerade für Theme-Entwickler und im Bezug auf die Performance einer Seite ist dies sehr ungünstig. Wird ein zusätzliches Skript mit jQuery Abhängigkeit in den Footer eingebunden, bleibt jQuery weiterhin im Header.

Um jQuery trotzdem in den Footer zu bekommen, wird im Netz recht häufig die Variante über wp_deregister_script( 'jquery' ) angepriesen. — Erst die originale Skriptdefinition löschen und dann wieder mit dem letzten Parameter neu definieren. Sehr unflexibel, da für die Neuregistrierung die jQuery Version und der Pfad zur Datei gebraucht wird.

Aus diesem Grund möchte ich nun auf die elegantere Variante eingehen.

PHP / RAW / github:gist
<?php
/**
 * Prints jQuery in footer on front-end.
 */
function ds_print_jquery_in_footer( &$scripts) {
	if ( ! is_admin() )
		$scripts->add_data( 'jquery', 'group', 1 );
}
add_action( 'wp_default_scripts', 'ds_print_jquery_in_footer' );

Bei dieser Methode klinkt man sich in den wp_default_scripts Hook ein. Dieser übergibt als Parameter ein Objekt vom Typ WP_Scripts, wo unter anderem die registrierten Skripte enthalten sind.
Die Definitionen zu jQuery sind daher in $scripts->registered['jquery'] deklariert:

[jquery] => _WP_Dependency Object
	(
		[handle] => jquery
		[src] => /wp-includes/js/jquery/jquery.js
		[deps] => Array
 			(
			)
 		[ver] => 1.7.2
		[args] => 
		[extra] => Array
			(
			)
	)

Damit WordPress jQuery nun im Footer einbindet, muss das Feld extra um dem Wert group => 1 ergänzt werden.
Dazu steht die Methode WP_Dependencies->add_data( $handle, $key, $value ) zur Verfügung.

Zusammengefasst ergibt sich schlussendlich $scripts->add_data( 'jquery', 'group', 1 );.

Wer eine PHP Version größer 5.3 einsetzt kann übrigens auch auf eine anonyme Funktion setzen:

PHP / RAW / github:gist
<?php
/**
 * Prints jQuery in footer on front-end.
 * For PHP > 5.3
 */
add_action(
	'wp_default_scripts',
	function( &$scripts ) {
		if ( ! is_admin() )
			$scripts->add_data( 'jquery', 'group', 1 );
	}
);

Zum Thema