Automatische Updates für WordPress-Themes, die nicht im Theme-Verzeichnis sind

Für Themes aus dem WordPress.org-Verzeichnis können Updates automatisch installiert werden, wenn eine neue Version verfügbar ist. Hier zeige ich euch, wie ihr dieses Verhalten für Themes umsetzen könnt, die sich nicht in dem Verzeichnis befinden.

Update vom 9. Februar 2017: Diese Lösung funktioniert auf Multisite-Installationen nicht.

Ich habe mich mit dem Thema vom eigenen Update-Mechanismus für Themes eigentlich nicht groß auseinandersetzen wollen. Die Suche nach einer Lösung für meinen Shop hat mich zu dem Plugin WooCommerce API Manager geführt, das ich zunächst eingesetzt habe. Von Anfang an hat mir nicht sonderlich gefallen, dass sich die Käufer bei dieser Variente registrieren müssen, damit es funktioniert. Als nun ein paar Probleme mit Child-Themes und doppelten Update-Benachrichtigungen aufgetaucht sind, habe ich mich im Plugin und WordPress-Core auf die Suche nach den Ursachen gemacht.

Nach einiger Zeit hatte ich dann zwar noch nicht alle Probleme behoben, aber verstanden, wie WordPress über Theme-Updates informiert wird. Also habe ich mich wegen meiner geringen Anforderungen an eine eigene Lösung gemacht.

Anforderungen an den Update-Mechanismus

Die Anforderungen sind nicht besonders groß. Ich muss nicht prüfen, wie viele Themes aus einem Kaufvorgang irgendwo aktiv sind, da ich die Download-Anzahl nicht beschränke. Folgende Punkte sollten aber erfüllt sein:

  • Die Lösung funktioniert mit dem normalen Download-Link, den WooCommerce nach dem Kauf eines digitalen Produkts an den Kunden verschickt.
  • Um den Update-Mechanismus zu aktivieren, fügt der Käufer einfach den Download-Link in ein Feld im Customizer ein.
  • Auch wenn ein Child-Theme des Kauf-Themes aktiv ist, wird nach Updates gesucht.
  • Nach dem Wechseln des Themes wird geprüft, ob das neue Theme ein Child-Theme des Kauf-Themes oder das Kauf-Theme selbst ist. Andernfalls wird der Download-Link gelöscht.

Bevor wir nun zu der Umsetzung der Lösung kommen, ein kurzer Abriss darüber, wie Theme-Updates in WordPress funktionieren.

Ablauf der Theme-Updates in WordPress

Für uns ist wichtig, dass es einen Transient update_themes gibt, in dem WordPress Informationen zu den aktuell installierten Themes sowie zu verfügbaren Aktualisierungen speichert. Der Inhalt dieses Transients sieht beispielsweise so aus, wenn alle Themes aktuell sind:

object(stdClass)#273 (4) {
    ["last_checked"]=>int(1485868202)
    ["checked"]=>array(7) {
        ["extant"]=>string(5) "1.0.1"
        ["rindby"]=>string(5) "1.1.3"
        ["schlicht-child/schlicht-child"]=>string(3) "1.0"
        ["schlicht"]=>string(5) "1.0.4"
        ["twentyfifteen"]=>string(3) "1.7"
        ["twentyfourteen"]=>string(3) "1.9"
        ["twentysixteen"]=>string(3) "1.3"
    }
    ["response"]=>array(0) {
    }
    ["translations"]=>array(0) {
    }
}

Wenn wir nun die Version von Extant manuell nach unten verändern, dann sieht die Ausgabe so aus (und wir bekommen einen Update-Hinweis im Backend):

object(stdClass)#273 (4) {
    ["last_checked"]=>int(1485868693)
    ["checked"]=>array(7) {
        ["extant"]=>string(3) "1.0"
        ["rindby"]=>string(5) "1.1.3"
        ["schlicht-child/schlicht-child"]=>string(3) "1.0"
        ["schlicht"]=>string(5) "1.0.4"
        ["twentyfifteen"]=>string(3) "1.7"
        ["twentyfourteen"]=>string(3) "1.9"
        ["twentysixteen"]=>string(3) "1.3"
    }
    ["response"]=>array(1) {
        ["extant"]=>array(4) {
            ["theme"]=>string(6) "extant"
            ["new_version"]=>string(5) "1.0.1"
            ["url"]=>string(36) "https://wordpress.org/themes/extant/"
            ["package"]=>string(54) "https://downloads.wordpress.org/theme/extant.1.0.1.zip"
        }
    }
    ["translations"]=>array(0) {
    }
}

Bei extant steht innerhalb von checked nun die Version 1.0 – im Theme-Verzeichnis ist die Version 1.0.1. Interessant ist nun der response-Teil, in dem die Informationen zu der neuen Version gespeichert sind. Darin befindet sich ein Array mit dem Slug des Themes als Schlüssel. Innerhalb dieses Arrays gibt es ein weiteres Array mit vier Einträgen:

  • theme enhält wieder den Theme-Slug. Soweit ich das beurteilen kann, ist dieser Teil optional.
  • new_version beinhaltet die neue Versionsnummer.
  • url gibt eine URL an, die in dem Overlay eingebunden wird, nachdem auf Details der Version 1.0.1 anzeigen geklickt wurde (optional).
  • package ist die URL zu dem ZIP-Archiv mit den neuen Theme-Dateien (ebenfalls optional). Ohne diese Angabe wird nur ein Hinweis auf das Update angezeigt, ohne eine direkte Update-Möglichkeit.

Um WordPress auf ein Theme-Update aufmerksam zu machen, müssen also lediglich die richtigen Informationen in das response-Array eingefügt werden. Um alles weitere kümmert sich die Theme-Update-Routine des WordPress-Core. Für die Anpassung dieses Transients gibt es den Filter pre_set_site_transient_update_themes, und damit sind wir bereit für die Umsetzung.

Umsetzung des Update-Skripts

Wir benötigen für unsere Lösung folgendes:

  1. Eine öffentlich zugängliche Seite mit den Metadaten der neuesten Theme-Version, damit das Update-Skript auf eine neue Version prüfen kann. Notwendig ist hier mindestens die Angabe von new_version.
  2. Customizer-Option, in die der Nutzer seinen Download-Link eingibt. Dieser wird als package-Angabe genutzt.
  3. An den Hook pre_set_site_transient_update_themes übergebene Funktion, die auf Updates prüft und gegebenenfalls den Transient anpasst, um die Theme-Update-Routine von WordPress in Gang zu setzen.
  4. Funktion für den switch_theme-Hook, die den Wert der Customizer-Option löscht, wenn weder das Kauf-Theme noch ein Child-Theme davon aktiv ist.

Metadaten-Seite erstellen

Letztlich ist es egal, wie diese Metadaten-Seite erstellt wird. Ihr könnt einfach eine statische JSON-Datei auf euren Server hochladen und dort immer die aktuellen Daten eintragen. Ich habe hier auf meiner Site einen Custom-Post-Type für die WordPress-Themes erstellt. Mit ein bisschen Bastelei und Custom Fields habe ich es hinbekommen, dass jede Einzelseite noch eine Unterseite changelog/ hat, auf der die Einträge einiger Custom Fields angezeigt werden (als Beispiel der Changelog von Schlicht).

Da ich bei diesem Changelog sowieso die Versionen hinterlege, habe ich mir eine weitere Unterseite upgrade-json/ erstellt. Diese Metadaten-Seite des Schlicht-Themes sieht so aus:

{
  "new_version": "1.0.4",
  "url": "https://florianbrinkmann.com/wordpress-themes/schlicht/changelog/",
  "theme_id": 2936
}

Der Wert theme_id ist die ID des WooCommerce-Produkts und wird später dafür genutzt, um möglichst sicher zu testen, dass der Nutzer eine URL zu einer Schlicht-ZIP eingibt. Funktionieren tut der Update-Prozess nämlich mit jeder Theme-ZIP, die angegeben wird – es ist nur nachher eventuell das Kauf-Theme von einem anderen Theme überschrieben.

Das hier ist die Funktion, die diesen JSON-Code erzeugt:

function fbn_theme_upgrade_json() {
    $theme_id = get_field( 'woocommerce-product' );
    if ( have_rows( 'changelog' ) ) {
        while ( have_rows( 'changelog' ) ) {
            the_row();
            $version       = get_sub_field( 'version' );
            $changelog_url = get_the_permalink() . 'changelog/';
            $version_data  = array(
                'new_version' => $version,
                'url'         => $changelog_url,
                'theme_id'    => $theme_id,
            );
            wp_send_json( $version_data );
        }
    }
}

Zuständig für die JSON-Ausgabe ist wp_send_json(). Alle weiteren Arbeiten müssen in dem Theme vorgenommen werden, das Aktualisierungen erhalten soll.

Customizer-Option für die Update-URL

Erstellen wir zunächst die Customizer-Option, in die der Kunde die Update-URL eingeben kann. Wir platzieren sie im Customizer-Bereich schlicht_options, der von dem Theme bereits erstellt wird und deshalb in diesem Code nicht mehr vorkommen muss:

/**
 * Customizer settings for theme update
 *
 * @param WP_Customize_Manager $wp_customize The Customizer object.
 */
function schlicht_update_customize_register( $wp_customize ) {
    $wp_customize->add_setting( 'schlicht_upgrade_url', array(
        'type'              => 'option',
        'default'           => '',
        'sanitize_callback' => 'schlicht_esc_update_url'
    ) );

    $wp_customize->add_control( 'schlicht_upgrade_url', array(
        'priority' => 1,
        'type'     => 'url',
        'section'  => 'schlicht_options',
        'label'    => __( 'Paste your download link for »Schlicht« to enable automatic theme updates.', 'schlicht' ),
    ) );
}

add_action( 'customize_register', 'schlicht_update_customize_register', 12 );

Wichtig ist, dass als type in der add_setting()-Methode option angegeben wird. So ist die Option nicht Theme-spezifisch, sodass die Updates auch noch funktionieren, wenn ein Child-Theme aktiviert ist – egal, ob die URL mit dem aktiven Parent- oder dem Child-Theme eingegeben wurde. Als sanitize_callback könntet ihr hier einfach esc_url_raw angeben, ich möchte aber prüfen, ob die URL mit dem Pattern https://florianbrinkmann.com/?download_file= beginnt. Das ist der Anfang eines Download-Links von WooCommerce für meinen Shop.

Die Sanitize-Callback-Funktion sieht so aus:

/**
 * Escape URL and check if it matches format https://florianbrinkmann.com/?download_file=
 *
 * @param $url
 *
 * @return string
 */
function schlicht_esc_update_url( $url ) {
    $url     = esc_url_raw( $url );
    $pattern = '|^(https://florianbrinkmann.com/?download_file=)|';
    preg_match( $pattern, $url, $matches );

    if ( ! empty ( $matches ) ) {
        return $url;
    } else {
        return '';
    }
}

Zunächst wird esc_url_raw() angewendet und danach geprüft, ob das genannte Pattern in der eingegebenen URL vorkommt. Ist das der Fall, wird die URL zurückgegeben, andernfalls ein leerer String.

Prüfung auf neue Updates und Veränderung des Transients

Kommen wir nun zum interessanten Part. Teilweise ist der folgende Code von dem Code des »WooCommerce API Manager«-Plugins inspiriert:

/**
 * Checking for updates and updating the transient for theme updates
 *
 * @param $transient
 *
 * @return mixed
 */
function schlicht_theme_update( $transient ) {
    if ( empty( $transient->checked ) ) {
        return $transient;
    }

    $request = schlicht_fetch_data_of_latest_version();
    if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) != 200 ) {
        return $transient;
    } else {
        $response = wp_remote_retrieve_body( $request );
    }

    $data     = json_decode( $response );
    $theme_id = $data->theme_id;
    unset( $data->theme_id );
    if ( version_compare( $transient->checked['schlicht'], $data->new_version, '<' ) ) { $transient->response['schlicht'] = (array) $data;

        $theme_package = get_option( 'schlicht_upgrade_url' );
        if ( ! empty ( $theme_package ) ) {
            $pattern = '|^(https://florianbrinkmann.com/?download_file=' . $theme_id . ')|';
            preg_match( $pattern, $theme_package, $matches );
            if ( ! empty ( $matches ) ) {
                $transient->response['schlicht']['package'] = $theme_package;
            } else {
            }
        }
    }

    return $transient;
}

add_filter( 'pre_set_site_transient_update_themes', 'schlicht_theme_update' );

Nach der Prüfung, ob der checked-Eintrag des Transients leer ist, wird in $request das Ergebnis eines wp_safe_remote_get()-Aufrufs aus der schlicht_fetch_data_of_latest_version() gespeichert. Das ist die schlicht_fetch_data_of_latest_version():

/**
 * Fetch data of latest theme version
 *
 * @return array|WP_Error
 */
function schlicht_fetch_data_of_latest_version() {
    $request = wp_safe_remote_get( 'https://florianbrinkmann.com/wordpress-themes/schlicht/upgrade-json/' );

    return $request;
}

Das Ergebnis sind die Metadaten, die wir weiter vorne vorbereitet haben. Wenn mit dem Ergebnis des Aufrufs alles okay ist, holen wir uns mit wp_remote_retrieve_body( $request ); den Body der Anfrage und speichern ihn in $response. Durch json_decode() machen wir ein Objekt daraus, speichern die WooCommerce-ID des Themes und löschen diesen Eintrag aus dem Objekt.

Über version_compare() vergleich wir anschließend, ob die aktuell installierte Version des Themes kleiner ist als die aus den ausgelesenen Metadaten. In diesem Fall speichern wir diese Metadaten (new_version und url) als Array in dem response-Eintrag für das Theme. Über get_option( 'schlicht_upgrade_url' ); holen wir uns den Wert des Upgrade-URL-Feldes und prüfen, wenn es nicht leer ist, erneut auf ein Pattern. Dieses Mal hängen wir aber noch die ID des Themes an, da beispielsweise eine Download-URL für das WooCommerce-Produkt mit der ID 123 mit https://florianbrinkmann.com/?download_file=123 beginnen würde. Diese Prüfung lasse ich im Customizer weg, um unnötige wp_safe_remote_get()-Anfragen zu vermeiden.

Wenn das Muster passt, wird die URL als package eingetragen und der Transient am Ende der Funktion zurückgegeben. Sollte das URL-Pattern nicht passen, wird dennoch der Update-Hinweis angezeigt, nur ohne Möglichkeit der Aktualisierung.

Upgrade-URL nach Theme-Wechsel löschen

Für die Löschung der Upgrade-URL bedienen wir uns der switch_theme-Aktion. Dabei können wir über wp_get_theme() das Objekt des neuen Themes bekommen und auf die template-Eigenschaft prüfen. Die entspricht dem Slug des Themes oder des Parent-Themes, falls es sich um ein Child-Theme handelt. Bei dem Theme Schlicht ist der Wert also schlicht.

Das hier ist die entsprechende Funktion:

/**
 * Remove upgrade URL option after switching the theme,
 * if the new theme is not schlicht or a child theme of schlicht
 */
function schlicht_remove_upgrade_url() {
    $theme_object = wp_get_theme();
    $template     = $theme_object->template;
    if ( $template == 'schlicht' ) {

    } else {
        delete_option( 'schlicht_upgrade_url' );
    }
}

add_action( 'switch_theme', 'schlicht_remove_upgrade_url', 10, 2 );

Wenn $theme_object->template den Wert schlicht hat, dann ist entweder das Theme Schlicht oder ein Child-Theme aktiv. Ist das nicht der Fall, wird der Wert der Customizer-Option mit delete_option( 'schlicht_upgrade_url' ); entfernt.

Damit hätten wir es geschafft und automatische Updates für Themes umgesetzt, die nicht aus dem W.org-Verzeichnis kommen.

Das könnte auch interessant sein

Schreib einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.