Ein Add-on für das Icons-Panel bauen
Schritt für Schritt: ein kleines Plugin, das Easy SVG prüft, SVG mit easy_svg_sanitizer() säubert und dem Icons-Panel einen eigenen Tab gibt, ohne Build.
Tutorial · Easy SVG
In diesem Tutorial baust du My SVG Check, ein kleines Add-on-Plugin aus zwei Dateien. Es bringt einen Tab SVG check ins Icons-Panel: Du fügst ein SVG ein, und der Tab zeigt, was der Sanitizer der Site entfernt und wie das saubere Ergebnis aussieht. Unterwegs nutzt du jeden öffentlichen Erweiterungspunkt des Panels: die Versionskonstanten, easy_svg_sanitizer(), den Filter easy_svg_panel_menu_tabs, die Action easy_svg_panel_enqueue und window.EasySvgPanel.registerTab().
Einen Build-Schritt gibt es nicht. Das JavaScript nutzt wp.element, das React, das WordPress ohnehin mitbringt, und schlichte createElement-Aufrufe statt JSX.
Tabs anderer Add-ons zeichnet das Icons-Panel nur, solange Easy SVG Pro aktiv und lizenziert ist (so baut src/panel/App.jsx in Easy SVG 5.4.0 seine Tab-Liste). Das Add-on unten prüft das und trägt seinen Menüeintrag nur ein, wenn der Tab auch gezeichnet wird. Seine Sanitizer-Route funktioniert auf jeder Site mit Easy SVG. Brauchst du eine Panel-Erweiterung auf Sites ohne Pro, nimm stattdessen registerCollections().
Was du brauchst
- WordPress 6.6 oder neuer und PHP 8.0 oder neuer (dieselbe Untergrenze wie Easy SVG).
- Easy SVG mit
EASY_SVG_API1 oder höher (damit kameasy_svg_sanitizer()) undEASY_SVG_PANEL_API2 oder höher (Tab-Bereiche, Tab-Icons und eine Berechtigung pro Tab). Easy SVG 5.4.0 hat 3 und 3. - Easy SVG Pro, aktiv und lizenziert, damit du den Tab siehst.
- Einen Benutzer mit der Berechtigung
upload_files, um den Tab zu nutzen. Administratoren, Redakteure und Autoren haben sie.
Die Dateien
wp-content/plugins/my-svg-check/
├── my-svg-check.php Hauptdatei: Prüfungen, REST-Route, Menüeintrag, Einreihen
└── panel-tab.js der Tab, registriert über window.EasySvgPanel
Schritt für Schritt
Plugin anlegen und Easy SVG prüfen
Plugins laden in alphabetischer Reihenfolge, Easy SVG kann also nach deinem laden. Mach die Prüfungen deshalb an plugins_loaded, wenn alle Plugins da sind. Fehlt etwas, registrierst du nur einen Admin-Hinweis und hörst auf. Die Konstanten sind der Vertrag: Vergleich sie, nicht die Plugin-Version.
Ein SVG mit dem Sanitizer der Site säubern
Eine REST-Route nimmt das eingefügte Markup und schickt es durch easy_svg_sanitizer(). Die Funktion liefert einen frischen Sanitizer mit den Allow-Listen der Site und ihrer Einstellung für entfernte Verweise, oder null, wenn die Sanitizer-Bibliothek fehlt. Der Sanitizer kann false oder einen leeren String liefern oder bei XML ohne genau eine svg-Wurzel eine Ausnahme werfen, deshalb werden alle drei Fälle behandelt. getXmlIssues() listet, was entfernt wurde.
Einen Menüeintrag mit eigener Berechtigung anlegen
Der Filter easy_svg_panel_menu_tabs gibt dem Tab einen Link unter Icons und die Berechtigung upload_files. Der Schlüssel muss dieselbe ID sein, die das JavaScript registriert.
Das Tab-Script einreihen
An easy_svg_panel_enqueue wird das Script im Footer eingereiht, mit dem übergebenen Panel-Handle, wp-element und wp-i18n als Abhängigkeiten.
Den Tab registrieren
panel-tab.js registriert den Tab mit ID, Label, Bereich, Reihenfolge, Icon und einer render-Funktion, die ein Element seiner eigenen Komponente liefert.
Ausprobieren
Aktivier das Plugin, öffne Icons, dann SVG check, und füg ein SVG mit einem script-Element ein.
Die Hauptdatei des Plugins
<?php
/**
* Plugin Name: My SVG Check
* Description: Adds an "SVG check" tab to the Easy SVG Icons panel.
* Version: 1.0.0
* Requires at least: 6.6
* Requires PHP: 8.0
* Text Domain: my-svg-check
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
const MYSVGCHECK_VERSION = '1.0.0';
const MYSVGCHECK_TAB = 'mysvgcheck';
const MYSVGCHECK_CAP = 'upload_files';
const MYSVGCHECK_MAX_BYTES = 512000;
add_action( 'plugins_loaded', 'mysvgcheck_boot' );
/**
* Hängt alles ein, sobald alle Plugins geladen sind.
*/
function mysvgcheck_boot() {
if ( ! mysvgcheck_easy_svg_ready() ) {
add_action( 'admin_notices', 'mysvgcheck_missing_notice' );
return;
}
add_action( 'rest_api_init', 'mysvgcheck_register_route' );
add_filter( 'easy_svg_panel_menu_tabs', 'mysvgcheck_menu_tab' );
add_action( 'easy_svg_panel_enqueue', 'mysvgcheck_enqueue' );
}
/**
* Ist Easy SVG aktiv und neu genug für dieses Add-on?
*
* @return bool
*/
function mysvgcheck_easy_svg_ready() {
return defined( 'EASY_SVG_API' )
&& defined( 'EASY_SVG_PANEL_API' )
&& 1 <= (int) EASY_SVG_API
&& 2 <= (int) EASY_SVG_PANEL_API
&& function_exists( 'easy_svg_sanitizer' );
}
/**
* Sagt Administratoren, warum das Add-on nichts tut.
*/
function mysvgcheck_missing_notice() {
if ( ! current_user_can( 'activate_plugins' ) ) {
return;
}
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
esc_html__( 'My SVG Check needs Easy SVG with a current version of its add-on contract. Install, activate or update Easy SVG.', 'my-svg-check' )
);
}
/**
* Ob das Icons-Panel einen Tab eines anderen Add-ons zeichnet.
*
* Spiegelt includes/panel.php und src/panel/App.jsx in Easy SVG 5.4.0:
* zusätzliche Tabs erscheinen nur, solange Easy SVG Pro aktiv ist.
*
* @return bool
*/
function mysvgcheck_panel_draws_tab() {
return defined( 'EASY_SVG_PRO_VERSION' );
}
/**
* Die REST-Route, die der Tab aufruft.
*/
function mysvgcheck_register_route() {
register_rest_route(
'my-svg-check/v1',
'/clean',
array(
'methods' => 'POST',
'callback' => 'mysvgcheck_clean',
'permission_callback' => 'mysvgcheck_can_use',
'args' => array(
'markup' => array(
'type' => 'string',
'required' => true,
),
),
)
);
}
/**
* Dieselbe Berechtigung wie der Menüeintrag. Die REST-Schicht prüft den
* Header X-WP-Nonce vorher, eine Anfrage ohne ihn ist also anonym.
*
* @return bool
*/
function mysvgcheck_can_use() {
return current_user_can( MYSVGCHECK_CAP );
}
/**
* Säubert das geschickte SVG und meldet, was der Sanitizer entfernt hat.
*
* @param WP_REST_Request $request Die Anfrage.
* @return WP_REST_Response|WP_Error
*/
function mysvgcheck_clean( WP_REST_Request $request ) {
$markup = (string) $request->get_param( 'markup' );
// Der Sanitizer parst das ganze Dokument, also die Eingabe begrenzen.
if ( strlen( $markup ) > MYSVGCHECK_MAX_BYTES ) {
return new WP_Error(
'mysvgcheck_too_large',
__( 'That SVG is too large to check here.', 'my-svg-check' ),
array( 'status' => 413 )
);
}
$sanitizer = easy_svg_sanitizer();
if ( null === $sanitizer ) {
return new WP_Error(
'mysvgcheck_no_sanitizer',
__( 'The Easy SVG sanitizer is not available.', 'my-svg-check' ),
array( 'status' => 503 )
);
}
try {
$clean = $sanitizer->sanitize( $markup );
} catch ( \Throwable $e ) {
// Wohlgeformtes XML ohne genau eine svg-Wurzel wirft eine Ausnahme.
$clean = false;
}
if ( ! is_string( $clean ) || '' === trim( $clean ) ) {
return new WP_Error(
'mysvgcheck_not_svg',
__( 'That is not an SVG the sanitizer accepts.', 'my-svg-check' ),
array( 'status' => 422 )
);
}
$removed = array();
foreach ( (array) $sanitizer->getXmlIssues() as $issue ) {
$removed[] = array(
'message' => sanitize_text_field( (string) ( $issue['message'] ?? '' ) ),
'line' => (int) ( $issue['line'] ?? 0 ),
);
}
return new WP_REST_Response(
array(
'clean' => $clean,
'removed' => $removed,
)
);
}
/**
* Trägt den Menüeintrag mit eigener Berechtigung ein, wenn der Tab gezeichnet wird.
*
* @param array $tabs Tab-ID zu Label oder zu Label und Berechtigung.
* @return array
*/
function mysvgcheck_menu_tab( $tabs ) {
$tabs = (array) $tabs;
if ( ! mysvgcheck_panel_draws_tab() ) {
return $tabs;
}
$tabs[ MYSVGCHECK_TAB ] = array(
'label' => __( 'SVG check', 'my-svg-check' ),
'capability' => MYSVGCHECK_CAP,
);
return $tabs;
}
/**
* Lädt das Tab-Script ins Icons-Panel.
*
* @param string $handle Das Script-Handle des Panels.
*/
function mysvgcheck_enqueue( $handle ) {
wp_enqueue_script(
'my-svg-check-panel',
plugins_url( 'panel-tab.js', __FILE__ ),
array( $handle, 'wp-element', 'wp-i18n' ),
MYSVGCHECK_VERSION,
true
);
wp_set_script_translations( 'my-svg-check-panel', 'my-svg-check', plugin_dir_path( __FILE__ ) . 'languages' );
}
Das Tab-Script
/* global window */
( function () {
'use strict';
const panel = window.EasySvgPanel;
if ( ! panel || 'function' !== typeof panel.registerTab ) {
return;
}
const { createElement: el, useState } = window.wp.element;
const { __ } = window.wp.i18n;
async function cleanSvg( ctx, markup ) {
const response = await window.fetch( ctx.restRoot + 'my-svg-check/v1/clean', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': ctx.nonce,
},
body: JSON.stringify( { markup } ),
} );
const data = await response.json().catch( () => ( {} ) );
if ( ! response.ok ) {
throw new Error( data.message || __( 'The check failed.', 'my-svg-check' ) );
}
return data;
}
function Removed( { items } ) {
if ( ! items.length ) {
return el( 'p', null, __( 'Nothing was removed. This SVG is clean.', 'my-svg-check' ) );
}
return el(
'ul',
null,
items.map( ( item, index ) =>
el( 'li', { key: index }, item.message + ' (' + __( 'line', 'my-svg-check' ) + ' ' + item.line + ')' )
)
);
}
function SvgCheck( { ctx } ) {
const [ markup, setMarkup ] = useState( '' );
const [ result, setResult ] = useState( null );
const [ error, setError ] = useState( '' );
const [ busy, setBusy ] = useState( false );
async function check() {
setBusy( true );
setError( '' );
setResult( null );
try {
setResult( await cleanSvg( ctx, markup ) );
} catch ( e ) {
setError( e.message );
} finally {
setBusy( false );
}
}
return el(
'div',
{ className: 'my-svg-check' },
el( 'p', null, __( 'Paste an SVG to see what this site\'s sanitizer removes from it. Nothing is saved.', 'my-svg-check' ) ),
el( 'textarea', {
rows: 10,
style: { width: '100%' },
value: markup,
'aria-label': __( 'SVG markup', 'my-svg-check' ),
onChange: ( event ) => setMarkup( event.target.value ),
} ),
el(
'p',
null,
el(
'button',
{
type: 'button',
className: 'button button-primary',
disabled: busy || '' === markup.trim(),
onClick: check,
},
busy ? __( 'Checking…', 'my-svg-check' ) : __( 'Check SVG', 'my-svg-check' )
)
),
error ? el( 'p', { role: 'alert' }, error ) : null,
result
? el(
'div',
null,
el( 'h3', null, __( 'Removed', 'my-svg-check' ) ),
el( Removed, { items: result.removed || [] } ),
el( 'h3', null, __( 'Clean result', 'my-svg-check' ) ),
el( 'textarea', {
rows: 10,
style: { width: '100%' },
readOnly: true,
value: result.clean,
'aria-label': __( 'Clean SVG markup', 'my-svg-check' ),
} )
)
: null
);
}
panel.registerTab( {
id: 'mysvgcheck',
label: __( 'SVG check', 'my-svg-check' ),
area: 'inspect',
order: 30,
// Ein Haken, gezeichnet auf dem 24 mal 24 Icon-Raster des Panels.
icon: el( 'path', { d: 'M5 12l4 4L19 7' } ),
render: ( ctx ) => el( SvgCheck, { ctx } ),
} );
} )();
Wie die Teile zusammenhängen
Worauf sich jeder Teil verlässt:
- Die Prüfungen lesen
EASY_SVG_APIundEASY_SVG_PANEL_API, die beiden Zahlen, die laut readme nur dann wechseln, wenn sich die Form des Vertrags ändert. - Der Menüeintrag macht den Tab über Icons erreichbar und nennt dem Panel seine Berechtigung. Das Panel gibt dem Browser die Berechtigung jedes Tabs und die des aktuellen Benutzers mit und zeichnet den Tab nur für Benutzer mit
upload_files. - Das Einreihen nutzt das Handle, das es bekommt. Weil
panel-tab.jsdavon abhängt, führt der Browser zuerst das Panel-Bundle aus, und dessen erste Anweisung legtwindow.EasySvgPanelan. renderliefert ein Element vonSvgCheckund sonst nichts. Das Panel ruftrenderbei jedem Rendern für jeden Tab auf, Zustand und Anfragen leben also in der Komponente, die das Panel nur einhängt, solange der Tab offen ist.ctx.restRootundctx.noncekommen vom Panel.restRootendet schon mit einem Schrägstrich und funktioniert auch mit einfachen Permalinks.
Wer was sieht
| Benutzer | Menü Icons | Gezeigte Tabs | Darf die Route aufrufen |
|---|---|---|---|
| Administrator | ja | alle Tabs, auch SVG check | ja |
| Redakteur oder Autor | ja | nur SVG check | ja |
| Mitarbeiter oder Abonnent | nein | keine | nein, 403 |
| Jeder, ohne lizenziertes Pro | Menü ohne SVG check | Panel ohne den Tab | ja, mit upload_files |
Das Panel öffnet sich für jeden, der mindestens einen Tab sehen darf. Redakteure und Autoren sehen nur diesen Tab, weil die eigenen Tabs von Easy SVG manage_options verlangen.
Testen
Installieren
Leg beide Dateien nach wp-content/plugins/my-svg-check/ und aktivier My SVG Check. Ohne Easy SVG solltest du auf der Plugin-Seite den Warnhinweis sehen.
Den Tab öffnen
Mit lizenziertem Easy SVG Pro öffnest du Icons, dann SVG check. Der Tab sitzt im Bereich Prüfen der Seitenleiste.
Ein unsicheres SVG prüfen
Füg das hier ein und klick auf Check SVG:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><script>alert(1)</script><path d="M4 12h16"/></svg>
Die Liste zeigt Suspicious tag 'script' mit seiner Zeile, und das saubere Ergebnis behält nur den Pfad.
Als Redakteur prüfen
Melde dich als Redakteur an. Das Menü Icons zeigt SVG check und sonst nichts.
Fehlersuche
Stolperfallen
- Prüf die Konstanten an
plugins_loaded, nicht beim Laden deiner Datei. Easy SVG ist zu diesem Zeitpunkt vielleicht noch nicht geladen. - Menüeintrag und JavaScript-ID müssen gleich sein, hier
mysvgcheck, sonst schlägt das Panel die falsche Berechtigung nach. - Die Berechtigung im Menüeintrag blendet den Tab aus, sie schützt die Route nicht. Geschlossen hält die Route
mysvgcheck_can_use(). - Zeig das saubere Ergebnis nicht mit
dangerouslySetInnerHTML. Das Beispiel steckt es in ein schreibgeschütztes Textfeld, dort ist es Text. Willst du eine Vorschau, render nur das Ergebnis des serverseitigen Sanitizers, wie hier, und nie die ursprüngliche Eingabe. - Begrenz die Eingabe. Der Sanitizer parst das ganze Dokument. Der Uploader von Easy SVG prüft Größe und Struktur ebenfalls vor dem Parsen; deine Route braucht eine eigene Grenze.
- Übersetzungen für
panel-tab.jslädtwp_set_script_translations()auslanguages/my-svg-check-<locale>-<md5>.json. Ohne solche Dateien erscheinen die englischen Strings.