Empfehlung des Tages: Filter-Funktionen

Seit PHP5.2 sind die Filter-Funktionen fester Bestandteil von PHP geworden. Jedoch greifen immer noch sehr wenige Programmierer auf diese Filter-Funktionen zurück, was ein kleines bis großes Sicherheitsrisiko darstellt.

Der Normalfall sieht immer noch so aus, dass einfach abgefragt wird ob in $_GET, $_POST oder $_REQUEST ein Schlüssel vorhanden ist und dann der entsprechende Wert verwendet wird. Leider viel zu oft sieht man auch, dass erst gar nicht geprüft wird ob der Schlüssel vorhanden ist und einfach davon ausgegangen wird das dieser vorhanden ist und verwendet werden kann. Das ist nicht nur schlechter Stil, sondern eröffnet auch eine Sicherheitslücke. Denn die superglobalen Arrays $_GET, $_POST und $_REQUEST sind keine Einbahnstraße, man kann aus ihnen lesen und in sie hinein schreiben. Schauen wir uns das mal etwas näher an.

$_REQUEST lassen wir in der in diesem Artikel mal außen vor da es noch nicht von den Filter-Funktionen unterstützt wird. Zudem sollte man auf $_REQUEST auch nur in Ausnahmefällen zugreifen. $_REQUEST Enthält die Daten von $_GET und $_POST. Die Verwendung von $_REQUEST deutet also darauf hin, dass man nicht so wirklich weiß was man tut und einfach auf gut Glück auf ein superglobales Array zugreift in der Hoffnung die gesuchten Daten seien schon irgendwie da.
Es gibt einige wenige Fälle wo der Zugriff auf $_REQUEST sinnvoll sein kann, man sollte sich jedoch im klaren darüber sein das der Zugriff auf $_REQUEST eben nicht so sicher ist als wenn man $_GET und $_POST mit den Filter-Funktionen abfragt.

$_GET und $_POST sind keine Einbahnstraße

Das man auf die superglobalen Arrays $_GET und $_POST nicht nur lesend, sondern auch schreibend zugreifen kann, ist anscheinend nicht allen klar. Schauen wir uns dazu mal dieses Script an

$_POST['nonce'] = 'badnonce';
echo 'Was the nonce send with a POST request?<br>';
echo 'With <code>isset()</code>: ' . ( isset( $_POST['nonce'] ) ? 'yes' : 'no' ) . '<br>';
echo 'With <code>filter_has_var()</code>' . ( filter_has_var( INPUT_POST, 'nonce' ) ? 'yes' : 'no' ) . '<br>';
echo 'Value of <code>nonce</code> with <code>$_POST[\'nonce\']</code>: ' . $_POST['nonce'] . '<br>';
echo 'Value of <code>nonce</code> with <code>filter_input( INPUT_POST, \'nonce\' )</code>:' . filter_input( INPUT_POST, 'nonce' ) . '<br>';

echo '<hr>';

$_GET['nonce'] = 'badnonce';
echo 'Was the nonce send with a GET request?<br>';
echo 'With <code>isset()</code>: ' . ( isset( $_GET['nonce'] ) ? 'yes' : 'no' ) . '<br>';
echo 'With <code>filter_has_var()</code>' . ( filter_has_var( INPUT_GET, 'nonce' ) ? 'yes' : 'no' ) . '<br>';
echo 'Value of <code>nonce</code> with <code>$_GET[\'nonce\']</code>: ' . $_GET['nonce'] . '<br>';
echo 'Value of <code>nonce</code> with <code>filter_input( INPUT_GET, \'nonce\' )</code>:' . filter_input( INPUT_GET, 'nonce' ) . '<br>';

Die nonces sind in WordPress ein Sicherheitsmechanismus mit dem man prüfen kann ob eine Anfrage tatsächlich von dort kommt, von wo man sie erwartet. Verlassen wir uns darauf das der Schlüssel nonce vorhanden ist und verwenden diesen, gehen wir das Risiko ein das er an einer anderen Stelle einfach überschrieben wurde.

Das man die Werte der superglobalen Arrays einfach überschreiben kann, lässt sich recht einfach mit vielen WordPress Funktionen testen die Werte aus den superglobalen Arrays $_GET, $_POST und/oder $_REQUEST verwenden.

add_action(
	'admin_init',
	function(){
		if (
			( isset( $_GET['action'] ) && 'delete-comment' == $_GET['action'] )
			||
			( isset( $_GET['action'] ) && 'trash' == $_GET['action'] )
		) {
			$_GET['action'] = $_POST['action'] = $_REQUEST['action'] = 'not-now';
		}
	}
);

Einfach mal diesen Code-Schnipsel in die functions.php einfügen und dann im Backend die Kommentarübersicht aufrufen. Dort dann wahllos ein paar Kommentare löschen. Entweder über die Checkboxen und eines der Bulk-Menüs oder über den Link der erscheint wenn man mit der Maus auf einen Kommentar zeigt.
Nutzt man die Checkboxen und das Bulk-Menü, passiert gar nichts. Beim Löschen über den Link verschwindet zwar der Kommentar und der Zähler hinter “Papierkorb” geht hoch, ein Klick auf “Alle Kommentare” genügt um die vermeintlich gelöschten Kommentare wieder anzuzeigen.

Es geht aber deutlich böser

add_action(
	'admin_init',
	function(){
		if (
			( isset( $_GET['action'] ) && 'delete-comment' == $_GET['action'] )
			||
			( isset( $_GET['action'] ) && 'trash' == $_GET['action'] )
		) {
			$a = array_fill( 0, 5, 0 );
			array_walk( $a, function( &$e ){ $e = rand( 1,999 ); } );
			$_GET['delete_comments'] = $_POST['delete_comments'] = $_REQUEST['delete_comments'] = $a;
			$_GET['c'] = $_POST['c'] = $_REQUEST['c'] = rand( 1, 999 );
		}
	}
);

Dieser Code überschreibt nicht einfach die action, sondern ersetzt die ausgewählten Kommentare durch zufällige Werte. Egal was man auswählt, welche Kommentaren gelöscht werden ist reiner Zufall. Im besten Fall passiert nichts, da die zufällig ausgewählten IDs nicht existieren. Im schlimmsten Fall werden ganz andere Kommentare gelöscht als ausgewählt. Man könnte auch über eine SQL-Abfrage alle vorhandenen Kommentar-IDS abfragen und dann zufällig IDs auswählen, dass ist dann richtig böse aber ein anderes Thema.

Mit filter_input() wäre dies nicht möglich, da filter_input() nur die Werte berücksichtigt, die auch tatsächlich über einen Request abgeschickt wurden (siehe erstes Beispiel). Nun fragt man sich vielleicht warum WordPress etwas so dummes macht wenn es doch eine solch einfache Lösung für das Problem gibt.
Nun die Antwort liegt in meinen ersten Satz dieses Artkels. Die Filter-Funktionen sind erst ab PHP5.2 verfügbar, WordPress wurde jedoch zu PHP4 Zeiten entwickelt und enthält auch noch unendlich viel PHP4-Code. Wer das ändern möchte, kann gerne mal eine Suche nach $_GET, $_POST und $_REQUEST über die WordPress Dateien laufen lassen und die entsprechenden Stellen anpassen. Die Suche nach $_GET liefert aktuell 594 Treffer in 104 Dateien. Soviel zu diesen Thema.

Ich hoffe es ist klar geworden warum es nicht ganz unerheblich ist seinen Code mindestens auf PHP5.2 Niveau zu bringen und warum man in seinen Themes und Plugins unbedingt auf die Filter-Funktionen setzen sollte anstatt direkt auf die superglobalen Arrays $_GET, $_POST und $_REQUEST zuzugreifen.

Zum Schluss noch etwas Mehrwert. Manchmal kann es vorkommen das Werte sowohl per GET als auch POST Request übergeben werden (meist im Zusammenhang mit Ajax Requests), in diesen Fall greift man gerne auf $_REQUEST zurück. Da die Filter-Funktionen von PHP $_REQUEST (noch) nicht unterstützen, müsste man jedes mal umständlich $_GET und $_POST abfragen ob der benötigte Schlüssel vorhanden ist. Das kann man sich mit einer kleinen Funktion vereinfachen und den Mangel von PHP gleich etwas ausbügeln.

function read_superglobals( $vars ) {
	
	if ( is_string( $vars ) )
		$vars = (array) $vars;
	
	$return = array();
	
	foreach ( $vars as $variable_name ) {
		if ( ! filter_has_var( INPUT_POST, $variable_name ) ) {
			if ( ! filter_has_var( INPUT_GET, $variable_name ) ) {
				$return[$variable_name] = '';
			} else {
				$return[$variable_name] = filter_input( INPUT_GET, $variable_name );
			}
		} else {
			$return[$variable_name] = filter_input( INPUT_GET, $variable_name );
		}
	}

	return $return;
}

Die Funktion erwartet als Parameter ein Array oder einen String mit den jeweiligen Schlüssel(n) den/die man abfragen möchte. Die Funktion prüft dann mit filter_input() ob in $_GET oder $_POST der Schlüssel vorhanden ist. Ist er vorhanden, kopiert sie den Wert mit den zugehörigen Schlüssel in ein Array und gibt anschließend das Array zurück. Die Funktion arbeitet in etwa so wie wp_reset_var(), jedoch mit den Unterschied das sie keine globalen Variablen erzeugt und ausschließlich die Schlüssel zurück gibt, die auch tatsächlich mittels eines GET oder POST Requests abgesendet wurden.
Natürlich kann man die Funktion auch dann verwenden, wenn man genau weiß ob die Werte mittels GET oder POST Request gesendet wurden. Dann bekommt man ein Array zurück mit exakt den Schlüssel-Werte Paaren die man benötigt.


Linktitel automatisch ausfüllen lassen

Derzeit schreibe ich anAutoinsert Linktitle ein paar Artikeln und ärgere mich immer das ich, wenn ich einen Link erstelle, den Titel für den Link manuell ausfüllen muss. Wahrscheinlich bin ich der einzige auf diesen Planeten mit diesen Problem, deswegen habe ich mir schnell mal ein kleines Plugin geschrieben.

Gibt man im URL-Feld eine gültige URL ein und verlässt dieses Feld, dann wird ein Ajax-Request abgesetzt der versucht aus der verlinkten Webseite den title-Tag zu extrahieren. Gelingt dies, wird der Webseitentitel in das Feld für den Linktitel eingefügt. Wenn nicht, passiert nix. Den Titel kann man bei Bedarf dann noch anpassen oder wieder löschen.

Vielleicht nimmt sich ja mal jemand etwas Zeit und gibt ein wenig Feedback, wäre schön und hilfreich.

Nachtrag

Ich hatte nicht damit gerechnet das dass Plugin ein so hohes Interesse wecken würde. Github, dort wo der Plugin-Code derzeit liegt, ist nicht jedem bekannt und auch nicht jeder weiß wie man aus dem Repository ein Zip bekommt. Deswegen hier eine kleine Anleitung dazu.

#111 Als erstes also mal das Github-Repository öffnen. Sollte euer Bildschirm anders aussehen als im Screenshot links, dann klickt auf Code (zwischen der Pulskurve und Network (die rote Markierung habe allerdings ich eingefügt)). Rechts neben Clone in Windows ist ein Button mit einer kleinen Wolke und dem Wort ZIP (siehe Screenshot). Klickt ihr da drauf, dann sollte eine Aufforderung erscheinen eine Zip-Datei zu speichern.
Diese Zip-Datei speichert ihr auf eurem Computer und kann dann bereits aus dem Backend von WordPress heraus installiert werden.
Dazu im Backend auf die Plugins-Seite gehen, dort oben neben Plugins auf Installieren klicken, im nächsten Bildschirm auf Hochladen (oder in der englischen Version Upload), die zuvor gespeicherte Zip-Datei auswählen und fertig. Nun noch das Plugin aktivieren und das war es.


Der kleine aber feine Unterschied zwischen is_email() und sanitize_email()

Wer mit E-Mails in WordPress arbeitet, sollte den Unterschied zwischen is_email() und sanitize_email() kennen. Er ist zwar klein, aber fein.

Der offensichtlichste Unterschied ist erst einmal der Rückgabewert. is_email() prüft ob eine gegebene E-Mail Adresse überhaupt den Kriterien entspricht und gibt einen String zurück wenn dem so ist. Andernfalls gibt is_email() false zurück. sanitize_email() hingegen gibt entweder einen String mit einer gültigen E-Mail Adresse oder einen leeren String zurück.
is_email() gibt also einen boolschen Wert oder einen String, sanitize_email() immer einen String zurück.

Nun könnte man mit einer einfachen Typumwandlung is_email() dazu bringen ebenfalls immer einen String zurück zu geben. (string) is_email( $email ) würde sich nahezu gleich verhalten wie sanitize_email(). Im Erfolgsfall eine gültige E-Mail Adresse, im Fehlerfall einen leeren String. Stimmt das? Überprüfen wir es:

$emails = array( 'foo@bar.com', 'baz-at-example-org', 'öttö@wördpäss.com', 'meh@cöm.com' );
echo '<ol>';
array_walk(
	$emails,
	function ($email) {
		$empty = 'an empty string';
		$is = (string) is_email( $email );
		$se = sanitize_email( $email );

		printf(
			'<li>%s - %s (%s)</li>',
			( '' != $is ) ? $is : $empty,
			( '' != $se ) ? $se : $empty,
             $email
		);
	}
);
echo '</ol>';

Die Ausgabe sieht in etwa so aus:

  1. foo@bar.com – foo@bar.com (foo@bar.com)
  2. an empty string – an empty string (baz-at-example-org)
  3. an empty string – tt@wrdpss.com (öttö@wördpäss.com)
  4. an empty string – meh@cm.com (meh@cöm.com)

Wie man sehen kann, gibt (string) is_email( $email ) wie erwartet bei ungültigen E-Mail Adressen einen leeren String zurück. Das WordPress glaubt das Umlaute in E-Mail Adressen nicht erlaubt seien, lass wir an dieser Stelle mal dahingestellt. WordPress kann halt (noch) nicht mit Umlautdomains umgehen.
Viel interessanter ist die Ausgabe von sanitize_email(). Auch sanitize_email() kann nicht mit Umlauten umgehen, streicht sie aber einfach aus der E-Mail Adresse anstatt sie z.B. zu kodieren.

Das führt nun natürlich zu Problemen wenn man es nicht weiß. Sollte mal eine größere Anzahl an E-Mails nicht versendet werden, kann das mitunter daran liegen das man vergessen hat vor dem Speichern zu prüfen ob WordPress damit umgehen kann.

// FALSCH
$user = array(
  'name' => 'Hans'
  'email' => sanitize_email( $email )
);
update_option( 'awesome_options', $user );

// Richtig
$user = array(
  'name' => 'Hans'
  'email' => sanitize_email( (is_email( $email ) )
);
update_option( 'awesome_options', $user );

Merke!

Jede E-Mail Adresse sollte bevor sie mit sanitize_email() verarbeitet wird erst mit is_email() validiert werden. Ansonsten bekommt man E-Mail Adressen die vielleicht lustig aussehen, es aber definitiv nicht sind.


Gimme-Gimme-Gimme

ABBA suchte dereinst nach einen Mann für die Nachtschicht. PHP kann damit wenig anfangen, genauso wenig wie mit den falschen Typen, wobei mit “Typen” jetzt nicht der Mann gemeint ist.

Oft schreibt man Funktionen oder Methoden und macht sich nur wenig Gedanken über den Rückgabewert im Fehlerfall, also dann, wenn die Funktion nicht die Aktion durchführen konnte wofür sie geschrieben wurde. Ich selber habe es bisher auch so gehandhabt das ich im Fehlerfall einen boolschen Wert oder vergleichbares zurück gegeben habe. Das Ergebnis ist, ich muss bei jeden Funktionsaufruf prüfen ob auch der Variablen-Typ zurück kommt den ich erwarte. Erwarte ich z.B. ein Array, kommt im Fehlkerfall aber ein boolsches False zurück, muss ich das abfangen.

Warum? Weil es ansonsten unter Umständen Fehlermeldungen hagelt und das Script abbricht. Im Backend kann man es noch verschmerzen, ich nenne das immer den “Weisste bescheid” Zustand. Im Frontend ist es aber mehr als peinlich und sogar kontraproduktiv wenn der Seitenaufbau nach dem Blogheader mit einer Fehlermeldung abbricht.

Stellen wir uns mal vor wir schreiben ein Widget für die Sidebar. Im Code für das Widget sind folgende zwei Funktionen:

/**
 * Create a ordered- or unordered list from values
 *
 * @param	string	$type	ol for ordered lists or ul for unordered list
 * @param	array	$values	Array with list items
 * @return	string			HTML list
 */
function get_list( $type = 'ol', $values = array() ) {

	$type = ( in_array( $type, array( 'ol', 'ul' ) ) ) ?
		$type : 'ol';

	foreach ( $values as &$val )
		$val .= ' item';

	$format = "<{$type}>" . str_repeat( '<li>%s</li>', sizeof( $values ) ) . "</{$type}>";

	return vsprintf( $format, $values );

}

/**
 * Returns an array or boolean depending on parameter
 *
 * @param	boolean	$fail	Returns an array if set to true, else returns boolean false.
 * @return Ambigous <boolean, multitype:string >
 */
function get_list_values( $fail = false ) {

	return ( true == $fail ) ?
		false : array( 'A', 'B', 'C' );

}

Die eine Funktion (get_list_values()) erzeugt ein Array mit Elementen, die andere Funktion (get_list()) erzeugt aus einem Array mit Werten eine HTML-Liste. Der Aufruf beider Funktionen könnte exemplarisch so aussehen:

$vals = get_list_values();
$list = get_list( 'ol', $vals );

echo $list;

Soweit funktioniert das ganze recht ordentlich. Nun provozieren wir jedoch mal einen Fehler und rufen die Funktion get_list_values() mit den Parameter 1 auf. Jetzt werden wir mit einen Warning: Invalid argument supplied for foreach() in ... begrüßt und das Script bricht an dieser Stelle ab.

Um solche Fehlermeldungen zu vermeiden, hätte ich vorher prüfen müssen ob $vals ein Array ist.

$vals = get_list_values(1);

if ( is_array( $vals ) ) {
  $list = get_list( 'ol', $vals );
  echo $list;
} else {
  // do some error handling
}

Das erscheint bisher alles recht logisch. Die frage ist nur, was soll man an dieser Stelle für ein Error-Handling machen? Einen Log-Eintrag? Eine eigene Fehlermeldung ausgeben? Nichts?
Nichts scheint mir eigentlich die beste Lösung zu sein. Mit einer eigenen Fehlermeldung kann der Besucher nichts anfangen.Und ein Log-Eintrag hilft dem Besucher auch nicht weiter. Ein Log-Eintrag hilft dem Programmierer bzw. den Admin an dieser Stelle ebenso wenig, da es unklar ist warum die Funktion get_list_values() gescheitert ist. Das kann uns nur die Funktion selber sagen, daher wäre ein Log-Eintrag innerhalb der Funktion sinnvoller untergebracht.

function get_list_values( $fail = false ) {

	if ( true == $fail ) {
		write_to_error_log( sprintf( 'Function %s was called with parameter $fail set to %s', __FUNCTION__, $fail ) );
		return false;
	}

	return array( 'A', 'B', 'C' );

}

Dadurch sind wir schon mal einen kleinen Schritt weiter. Der Fehler wird da geloggt, wo er tatsächlich auftritt, nicht da wo er zu einen weiteren Fehler führt. Das eigentliche Problem besteht jedoch weiterhin. Denn die Funktion gibt ja noch einen boolschen Wert zurück. Wir müssen also weiterhin prüfen ob das Ergebnis des Funktionsaufrufes tatsächlich von den erwarteten Typs ist. Das ist hinderlich, denn dadurch ist folgendes Konstrukt nicht möglich:

echo get_list( 'ol', get_list_values( 1 ) );

Würde die Funktion nun einen Wert vom erwarteten Typ zurück geben, könnten wir obiges Konstrukt ohne weiteres verwenden. Im Fehlerfall erhalten wir dann anstatt einer Liste lediglich ein leeres Ergebnis. Also ändern wir die Funktion ein klein wenig:

function get_list_values( $fail = false ) {

	if ( true == $fail ) {
		write_to_error_log( sprintf( 'Function %s was called with parameter $fail set to %s', __FUNCTION__, $fail ) );
		return array();
	}

	return array( 'A', 'B', 'C' );

}

Das Logging des Fehlers bleibt, er geht also nicht verloren. Dafür haben wir im Frontend keine Fehlermeldung mehr und der Seitenaufbau bricht auch nicht mehr ab.

Dies können wir nun auf alle Funktionen übertragen. Wenn die Funktion im Erfolgsfall ein Array zurück gibt, sollte sie dies auch im Fehlerfall machen, dann halt ein leeres Array. Erwarten wir ein Objekt als Rückgabewert, so sollte im Fehlerfall ein leeres Objekt zurück kommen. Allerdings sollte man speziell bei Objekten die Eigenschaften vorbelegen und nicht einfach nur ein leeres Objekt zurück geben da oft direkt auf die Objekteigenschaften zugegriffen wird und man, anders als bei Arrays, nicht mit empty() prüfen kann ob das Objekt überhaupt irgendwelche Eigenschaften enthält. Eine Abfrage mit empty() liefert bei Objekten immer false zurück, da ein Objekt niemals “leer” ist.

function get_some_object( $fail = false ) {

	$object = new stdClass();
	$object->foo = false;
	$object->bar = '';
	$object->baz = null;

	if ( true == $fail )
		return $object;

	$object->foo = true;
	$object->bar = 'FooBarBaz';
	$object->baz = $object;

	return $object;

}
$objects = array();
var_dump( $objects );

$objects[] = get_some_object();
$objects[] = get_some_object( true );
$objects[] = new stdClass();

array_walk( $objects, function( $object ) { var_dump( empty( $object ) ); } );

/*
Ausgabe:
array (size=0)
  empty

boolean false
boolean false
boolean false
*/

Der Grund warum man sich damit beschäftigen sollte ist jedoch nicht nur eine saubere Ausgabe im Frontend, sondern auch das Debuggen. Im ersten Beispiel wurde der Fehler nämlich still und heimlich an einen ganz anderen Ort verlagert. Obwohl er in der Funktion get_list_values() aufgetreten ist, wird er erst in get_list() beim Aufruf der foreach-Schleife wirksam. In Beispielen ist es immer einfach den Fehler zu finden. Sie sind von Natur aus kurz und übersichtlich. In größeren und längeren Scripten ist es jedoch oft nicht so einfach heraus zu finden wann und wo eine Variable durch einen Funktionsaufruf einen Wert zugewiesen bekommen hat. Liegen zwischen der Wertzuweisung und der Verarbeitung des Wertes noch andere Zwischenschritte in der der fehlerhafte Wert nicht zu einen Fehler führt, wird die Suche noch umständlicher.
Deshalb sollte man Fehlerfälle immer sofort dort behandeln, wo sie auftreten anstatt sie in einen Pseudozustand umzuwandeln und sie ggf. auch noch als Rückgabewert zurück zu geben.


Metaboxen eine CSS-Klasse zuordnen

Auf WPSE tauchte die Frage auf wie man eine Metabox im Backend per Voreinstellung minimiert darstellt. WordPress bietet hierfür keine Option an, obwohl es wahrscheinlich keine schlechte Idee wäre. Denn fügt man relativ viele Metaboxen ein, wird es schnell unübersichtlich. Um eine Metabox minimiert (geschlossen) darzustellen, benötigt sie die CSS-Klasse closed.
Auch wenn man eine Metabox in Abhängigkeit eines bestimmten Wertes besonders hervorheben möchte, z.B. weil eine Aktion beim Speichern fehlgeschlagen ist, bietet WordPress von Haus aus keine Option für zusätzliche CSS-Klassen an. Wer seine Metaboxen anders gestalten möchte, würde also wahrscheinlich auf JavaScript zurück greifen.

Die einfachste Lösung ist wie so oft ein Filter, der allerdings etwas versteckt ist. Die Funktion add_meta_box() selber hat keinerlei Filter oder Actions in die man sich einhängen könnte.
Dafür aber die Funktion die dafür zuständig ist die Titleleiste der Metabox darzustellen. Damit es etwas klarer ist um welchen Teil des HTMLs es hier geht, auszugsweise das HTML der Excerpt-Metabox:

<div id="postexcerpt" class="postbox">
  <div class="handlediv" title="Zum umschalten klicken">
    <h3 class="hndle">
      <span>Auszug</span>
    </h3>
   <div class="inside">
[...]
    </div> <!-- Ende div .inside -->
  </div> <!-- Ende div .handlediv -->
</div> <!-- Ende div #postexcerpt -->

Der Filter kann CSS-Klassen zum äußersten Div-Container, hier im Beispiel mit der ID postexcerpt, hinzufügen. Somit ist es dann auch möglich im Stylesheet die nachfolgenden HTML-Elemente zu stylen.

add_action( 'add_meta_boxes', 'add_my_metabox' );

function add_my_metabox() {
  $id       = 'my-metabox';
  $title    = 'My Metabox';
  $callback = 'my_metabox_content';
  $page     = 'post';

  add_meta_box( $id, $title, $callback, $page );

  add_filter( "postbox_classes_{$page}_{$id}", 'minify_my_metabox' );
}

function my_metabox_content() { ... }

/**
 * Add extra css-classes to a meta-box
 *
 * @param array $classes Array with css-classes
 */
function minify_my_metabox( $classes ) {
  array_push( $classes, 'closed' );

  return $classes;
}

In der Funktion add_my_metabox() wird zunächst einmal ganz normal eine Metabox erzeugt. Anschließend wird jedoch noch ein Filter gesetzt der als Callback die Funktion minify_my_metabox() aufruft. Der Hook für den Filter muss man um die ID der Metabox und den Post-Type ergänzen. An dieser Stelle wird es ein wenig kompliziert. Im Codex wird der Begriff “Post Type” verwendet, da der Filter und die zugehörige Funktion jedoch undokumentiert sind, muss man im Quellcode nachschauen. Und dort wird “page” als Variablenname verwendet.
Ich persönlich finde den Begriff “page” sinnvoller, da man mit der gleichen Methode auch z.B. die Widgets im Dashboard beeinflussen kann (siehe Beispiel unten). Als “page” müsste man dazu lediglich dashboard angeben und kann dann eigenen oder bereits vorhandenen Widgets, weitere CSS-Klassen hinzufügen. Im Codex findet man bei remove_meta_box() noch ein paar weitere mögliche Werte für page. Hier wird übrigens wieder von page gesprochen, im Gegensatz zu post type bei add_meta_box().

function register_dash_widget(){

	$id   = 'debugoutput';
	$page = 'dashboard';

	wp_add_dashboard_widget(
		$id,
		'Debug Output',
		function () { echo 'Hello World!'; }
	);

	add_filter(
		"postbox_classes_{$page}_{$id}",
		function ( $classes ) {
			array_push( $classes, 'closed' );
			return $classes;
		}
	);

}

add_action( 'wp_dashboard_setup', 'register_dash_widget' );

Die Callback-Funktion minify_my_metabox() ist dann wieder recht unspektakulär. Sie erwartet als einzigen Parameter ein Array mit den CSS-Klassen. Fügt man z.B. die Klasse closed hinzu und gibt das Array zurück, so wird die Metabox (oder das Widget) minimiert anstatt geöffnet dargestellt. Denkbar wäre es natürlich auch eine CSS-Klasse alert hinzuzufügen um eine Metabox (oder Widget) farblich hervorzuheben.