toscho.design

WordPress: Anonyme Hooks deaktivieren

Gegeben sei ein fremdes Plugin, das einen Filter setzt, den wir abschalten wollen. Beispielsweise so:

add_filter('comments_array',array( &$this, 'FbComments' ));

Welchen Namen hat dieser Filter? Wie sollte remove_filter() aussehen? Gar nicht so einfach. $this ist außerhalb der laufenden Instanz des Plugins nicht erreichbar.

Als einfaches Beispiel habe ich mal ein Worst-Case-Plugin gebaut. Es erzeugt eine Instanz, sobald es von WordPress eingebunden wurde. Auf die Instanz haben wir keinen unmittelbaren Zugriff, weil dafür die passende Funktion fehlt und nicht einmal eine globale Variable existiert.

<?php # -*- coding: utf-8 -*-
/* Plugin Name: Anonymous OOP Action */

if ( ! class_exists( 'Anonymous_Object' ) )
{
    /**
     * Add some actions with randomized global identifiers.
     */
    class Anonymous_Object
    {
        public function __construct()
        {
            add_action( 'wp_footer', array ( $this, 'print_message_1' ), 5 );
            add_action( 'wp_footer', array ( $this, 'print_message_2' ), 5 );
            add_action( 'wp_footer', array ( $this, 'print_message_3' ), 12 );
        }

        public function print_message_1()
        {
            print '<p>Kill me!</p>';
        }

        public function print_message_2()
        {
            print '<p>Me too!</p>';
        }

        public function print_message_3()
        {
            print '<p>Aaaand me!</p>';
        }
    }

    // Good luck finding me!
    new Anonymous_Object;
}

Nach dem Aktivieren sehen wir ungefähr dieses:

killer-messages

WordPress braucht einen Namen für den Filter. Wir haben keinen angegeben. Also ruft WordPress _wp_filter_build_unique_id() auf und baut sich selber einen. Der ist für uns nicht vorhersagbar.

var_export( $GLOBALS['wp_filter'][ 'wp_footer' ] );

… gibt jetzt etwas aus, das ungefähr so aussieht:

array (
  5 => 
  array (
    '000000002296220e0000000013735e2bprint_message_1' => 
    array (
      'function' => 
      array (
        0 => 
        Anonymous_Object::__set_state(array(
        )),
        1 => 'print_message_1',
      ),
      'accepted_args' => 1,
    ),
    '000000002296220e0000000013735e2bprint_message_2' => 
    array (
      'function' => 
      array (
        0 => 
        Anonymous_Object::__set_state(array(
        )),
        1 => 'print_message_2',
      ),
      'accepted_args' => 1,
    ),
  ),
  12 => 
  array (
    '000000002296220e0000000013735e2bprint_message_3' => 
    array (
      'function' => 
      array (
        0 => 
        Anonymous_Object::__set_state(array(
        )),
        1 => 'print_message_3',
      ),
      'accepted_args' => 1,
    ),
  ),
  20 => 
  array (
    'wp_print_footer_scripts' => 
    array (
      'function' => 'wp_print_footer_scripts',
      'accepted_args' => 1,
    ),
  ),
  1000 => 
  array (
    'wp_admin_bar_render' => 
    array (
      'function' => 'wp_admin_bar_render',
      'accepted_args' => 1,
    ),
  ),
)

Diese sehr langen Filternamen ('000000002296220e0000000013735e2bprint_message_1') müssen wir finden, um remove_filter() erfolgreich umzusetzen.

Um den Namen zu finden, müssen wir also alle Filter abgrasen, die mit dem Hook assoziiert wurden, jeden prüfen, ob er ein Array ist und das Objekt eine Instanz unserer Klasse, dann nehmen wir die Priorität und entfernen den Filter. Klingt ganz schön kompliziert. Ist es auch. Ich habe mir dafür einen Funktion gebaut:

if ( ! function_exists( 'remove_anonymous_object_filter' ) )
{
    /**
     * Remove an anonymous object filter.
     *
     * @param  string $tag    Hook name.
     * @param  string $class  Class name
     * @param  string $method Method name
     * @return void
     */
    function remove_anonymous_object_filter( $tag, $class, $method )
    {
        $filters = $GLOBALS['wp_filter'][ $tag ];

        if ( empty ( $filters ) )
        {
            return;
        }

        foreach ( $filters as $priority => $filter )
        {
            foreach ( $filter as $identifier => $function )
            {
                if ( is_array( $function)
                    and is_a( $function['function'][0], $class )
                    and $method === $function['function'][1]
                )
                {
                    remove_filter(
                        $tag,
                        array ( $function['function'][0], $method ),
                        $priority
                    );
                }
            }
        }
    }
}

Wann benutzen wir diese Funktion? Schwer zu sagen: Wir wissen ja nicht genau, wann der anonyme Filter aktiviert wird. Im Moment einfach dann, wenn WordPress das Plugin einbindet. Aber später könnte die Erzeugung des Objektes an eine andere Aktion gebunden werden …
Am sichersten ist es, wenn wir erst in den Hook einsteigen, bei dem wir den anderen Filter deaktivieren wollen. Und zwar sehr zeitig, also mit der Priorität 0. Wir bauen uns also eine spezialisierte Zusatzfunktion und hängen sie auch an wp_footer.

add_action( 'wp_footer', 'kill_anonymous_example', 0 );

function kill_anonymous_example()
{
    remove_anonymous_object_filter(
        'wp_footer',
        'Anonymous_Object',
        'print_message_3'
    );
}

Message 3 (Aaaand me!) ist jetzt weg. Für den oben genannten Fall der Facebook-Kommentare sähe das so aus:

add_action( 'comments_array', 'kill_FbComments', 0 );

function kill_FbComments()
{
    remove_anonymous_object_filter(
        'comments_array',
        'SEOFacebookComments',
        'FbComments'
    );
}

Was können wir als Plugin-Autoren daraus lernen?

Vermeide anonyme Objekte. Stelle immer einen standardisierten Zugriff auf deine Objekte bereit, wenn du sie in einem Filter oder einer Aktion benutzt. Das kann eine Methode get_instance() in der Klasse sein oder – eher unschön – eine globale Variable.
Und leg nicht einfach los, wenn WordPress das Plugin einbindet. Warte mindestens bis zur Aktion plugins_loaded.

Verwandte Artikel