Bilder-Check (serendipity_event_bilderscan)
Ein Serendipity-Event-Plugin, das alle Artikel nach eingebundenen Bildern
durchsucht und im Backend eine Übersicht über auffällige Bilder anzeigt:
zu große Dateien, fehlende Thumbnail-Verkleinerung, nicht mehr auffindbare
Dateien und JPEGs mit Datenmüll hinter dem eigentlichen Bildende. Optional
lassen sich große Bilder direkt aus der Übersicht heraus verkleinern bzw.
von diesem Datenmüll bereinigen; vor jeder Änderung wird automatisch ein
Backup angelegt, das sich über eine eigene Backup-Verwaltung einsehen,
zurückspielen oder löschen lässt. Außerdem findet das Plugin Zoom-Links
(<a>-Tags um verlinkte Originalbilder) ohne title-Attribut und hilft,
sie systematisch nachzupflegen.
Wie es sich ins Backend einhängt
Serendipity kennt zwei grundlegend verschiedene Plugin-Typen: Event-Plugins
(reagieren auf Events wie frontend_display oder Cronjobs) und Plugins mit
einer eigenen Konfigurationsseite. Dieses Plugin nutzt keinen der
"echten" Events – event_hook() gibt immer sofort false zurück und tut
nichts. Es klinkt sich stattdessen ausschließlich über die von jedem
Serendipity-Plugin bereitgestellte Konfigurationsseite ein:
- Im Backend unter Plugins konfigurieren → Bilder-Check → Konfigurieren
ruft Serendipity automatisch
performConfig(&$bag)auf, sobald diese Seite aufgerufen wird. - Alles, was auf dieser Seite sichtbar ist – Statistiken, Artikelliste,
Top-Liste, Verkleinern-Buttons – wird von
performConfig()direkt mitechoausgegeben. Es gibt keine Template-Datei; das HTML steht im PHP. - Die Formularfelder unter "Einstellungen" (Schwellenwerte, Zielgrößen, …)
werden von Serendipity selbst gerendert und gespeichert, basierend auf
introspect()undintrospect_config_item(). Das Plugin muss diese Werte inperformConfig()nur noch mit$this->get_config(...)auslesen.
Weil alles innerhalb von performConfig() passiert, hat das Plugin keinen
Einfluss auf das öffentliche Frontend – Besucher der Website sehen nichts
davon. Es ist ein reines Backend-/Admin-Werkzeug.
Sprachdateien: Haupt- vs. UTF-8/-Ordner
Ganz oben in der Plugin-Datei lädt @serendipity_plugin_api::load_language(dirname(__FILE__));
die passenden Übersetzungskonstanten (PLUGIN_EVENT_BILDERSCAN_NAME,
PLUGIN_EVENT_BILDERSCAN_DESC). Serendipity unterstützt seit jeher zwei
Zeichensätze für Installationen: das alte ISO-8859-15 und UTF-8 (heute der
Normalfall). load_language() schaut sich dafür an, welchen Zeichensatz
der Blog verwendet, und sucht bei UTF-8-Blogs gezielt in einem Unterordner
UTF-8/ des Plugins nach der passenden lang_xx.inc.php – die Dateien im
Plugin-Hauptordner gelten nur als Fallback für ISO-8859-15-Installationen.
Fehlt der UTF-8/-Ordner, findet Serendipity dort keine deutsche Datei und
fällt am Ende auf die eingebaute Fallback-Sprache Englisch zurück, obwohl
lang_de.inc.php im Hauptordner vorhanden und korrekt kodiert ist. Deshalb
gibt es hier lang_de.inc.php/lang_en.inc.php doppelt: einmal im
Plugin-Hauptordner und identisch nochmal unter UTF-8/.
Bedeutung der wichtigsten Methoden
| Methode | Aufgerufen von | Zweck |
|---|---|---|
introspect() |
Serendipity, beim Laden der Plugin-Liste | Meldet Name, Beschreibung und welche Konfigurationsfelder es gibt |
introspect_config_item() |
Serendipity, pro Konfigurationsfeld | Definiert Typ, Label, Beschreibung und Default jedes einzelnen Feldes |
generate_content() |
Serendipity (Pflichtmethode von serendipity_event) |
Wird für dieses Plugin praktisch nicht genutzt, liefert nur den Titel |
performConfig() |
Serendipity, beim Aufruf der Konfigurationsseite | Führt den kompletten Scan durch und rendert die Ergebnisseite |
event_hook() |
Serendipity, bei Frontend-/Backend-Events | Leer – dieses Plugin reagiert auf keine Events |
Wie der Scan im Detail funktioniert
Der gesamte Ablauf steckt in performConfig() und läuft bei jedem Aufruf
der Konfigurationsseite neu ab (kein Caching, keine Hintergrundjobs):
1. Artikel laden
Es wird direkt per SQL auf die bestehende Serendipity-Datenbankverbindung zugegriffen (keine eigenen Zugangsdaten nötig):
SELECT id, title, body FROM {dbPrefix}entries WHERE body LIKE '%<img%'
Nur Artikel, deren Text mindestens ein <img>-Tag enthält, werden überhaupt
betrachtet.
2. Bilder pro Artikel erkennen
Für jeden Artikeltext (body) werden per Regex zwei Dinge gesucht:
- Gepaarte Zoom-Links:
<a href="original.jpg"><img src="thumb.jpg"></a>– das typische Muster "kleines Thumbnail verlinkt auf großes Original". Aus jedem Treffer wird ein Bildpaar (thumb+original) gebildet. - Alle übrigen
<img src="...">, die nicht Teil eines solchen Paares sind – also einzeln eingebundene Bilder ohne Zoom-Link.
Jedes gefundene Bild (Thumbnail, Original oder Einzelbild) wird über
buildImageEntry() geprüft:
is_thumb: Enthält der Dateiname das konfigurierte Thumbnail-Erkennungsmuster (Standard:serendipityThumb)?abs/found: Auflösung des im Artikel gespeicherten Pfads (relativ oder absolute URL) in einen echten Dateisystempfad viaresolvePath(), und Prüfung, ob die Datei dort tatsächlich existiert.size: Dateigröße in Byte viafilesize(), falls die Datei gefunden wurde.width/height: Bildmaße in Pixel viagetimagesize(), falls die Datei gefunden wurde und lesbar ist.garbage_bytes: Bei JPEG-Dateien (Endung.jpg/.jpeg) ermitteltfindJpegRealEnd(), wo das Bild laut JPEG-Struktur wirklich endet (siehe unten). Ist die Datei länger als dieses echte Ende, steht die Differenz hier – das ist Datenmüll, der z. B. durch einen nicht sauber begrenzten Speicherpuffer bei App/Kamera oder beim Upload entstanden sein kann.
3. Auffälligkeiten bewerten
Ein Artikel gilt als "auffällig" ($hasIssue = true) und taucht in der
Ergebnisliste auf, sobald mindestens eines seiner Bilder eines der
folgenden Kriterien erfüllt:
- Ein als Thumbnail eingebundenes Bild trägt nicht das Thumbnail-Erkennungsmuster im Dateinamen (lädt vermutlich versehentlich das Original statt einer verkleinerten Version).
- Die Dateigröße liegt über dem jeweiligen Schwellenwert – dabei wird
unterschieden:
- Schwellenwert Thumbnails (KB) gilt für Thumbnails und für unverlinkte Einzelbilder.
- Schwellenwert Originalbilder (KB) gilt für die verlinkten Original-/Zoom-Bilder, die naturgemäß größer sein dürfen.
- Die verlinkte Datei wurde auf dem Server nicht gefunden (z. B. nach einem Umzug oder gelöschten Dateien).
- Bei JPEGs: Die Menge an Datenmüll hinter dem echten Bildende
(
garbage_bytes) liegt über dem Schwellenwert Datenmüll (KB).
Für Originalbilder wird ein fehlender Thumbnail-Marker nicht als Fehler gewertet, da Originale das Thumbnail-Muster erwartungsgemäß nicht tragen.
Diese vier Kriterien prüft isCritical() pro Bild; das Ergebnis (critical)
wird an jedem Bild gespeichert. Ein Artikel gilt als auffällig, sobald
mindestens eines seiner Bilder critical ist. In der Artikelansicht lässt
sich darüber ein Bild-Filter pro Artikel umschalten (siehe unten).
4. Ausgabe
performConfig() gibt direkt HTML aus (inkl. eines kleinen <style>-Blocks):
- Statistik-Kacheln: Anzahl auffälliger Artikel, geprüfte Bilder, Bilder ohne Thumb-Marker, nicht gefundene Dateien, Bilder mit Datenmüll am Dateiende.
- Artikelliste: Pro auffälligem Artikel eine Tabelle mit allen seinen
Bildern (Typ, Bildpfad, Status-Badge, Dateigröße, Maße in Pixel,
Datenmüll-Badge) sowie einem Link direkt zum Bearbeiten des Artikels im
Backend. Der Bildpfad selbst ist anklickbar und öffnet die Datei über
ihre öffentliche URL (
publicUrl()) in einem neuen Tab (target="_blank").- Enthält ein Artikel sowohl kritische als auch unkritische Bilder,
erscheint über der Tabelle die Checkbox „Nur kritische Bilder
anzeigen“. Das ist reines Client-seitiges JavaScript (kein
Neuladen der Seite): jede Bildzeile trägt ein Attribut
data-critical="1"/"0", und dasonchange-Handler der Checkbox blendet beim Aktivieren alle Zeilen mitdata-critical="0"perstyle.display = 'none'aus. Ohne aktivierte Checkbox sind wie bisher alle Bilder des Artikels sichtbar.
- Enthält ein Artikel sowohl kritische als auch unkritische Bilder,
erscheint über der Tabelle die Checkbox „Nur kritische Bilder
anzeigen“. Das ist reines Client-seitiges JavaScript (kein
Neuladen der Seite): jede Bildzeile trägt ein Attribut
- Top-N-Liste: Unabhängig von den Schwellenwerten werden alle
gefundenen Bilder nach Dateigröße sortiert; die N größten werden separat
aufgelistet (Konfigurationsfeld "Anzahl größte Bilder anzeigen"), inkl.
Dateigröße, Maße in Pixel und Datenmüll-Badge samt "Bereinigen"-Button.
Bei
0wird dieser Abschnitt komplett ausgeblendet. Gerade hier tauchen Dateien mit angehängtem Datenmüll typischerweise zuerst auf, weil sie dadurch unverhältnismäßig groß werden.
5. Verkleinern-Aktion
Jede Zeile mit einer gefundenen Datei hat einen "Verkleinern"-Button. Das
zugehörige <form> sendet bilderscan_action=resize, den Bildpfad und den
Typ (thumb/original) per POST an dieselbe Seite. performConfig()
erkennt das am Anfang der Methode und ruft resizeImage() auf, bevor
der eigentliche Scan läuft (damit die neue Dateigröße sofort mitgemessen
wird):
- Ermittelt die aktuelle Bildgröße via
getimagesize(). - Bricht ab, wenn das Bild bereits innerhalb der Zielgröße liegt (Feld "Zielgröße Thumbnails (px)" bzw. "Zielgröße Originalbilder (px)", je nach Bildtyp).
- Legt vor der ersten Änderung über
backupOriginal()eine Sicherungskopie der Originaldatei in einem Unterordner_originals_backupneben der Datei an (falls dort noch keine existiert – bestehende Backups werden nicht überschrieben, siehe Hinweis unten). - Skaliert das Bild proportional auf die längste Kante herunter
(
imagecopyresampled) und speichert es am selben Pfad neu (JPEG mit Qualität 82, PNG verlustfrei mit Kompressionsstufe 6, inkl. Erhalt von Transparenz).
6. Bereinigen-Aktion (Datenmüll entfernen)
Bilder mit erkanntem Datenmüll (siehe Schritt 2/3) haben zusätzlich einen
"Bereinigen"-Button – sowohl in der Artikelliste als auch in der
Top-N-Liste. Das zugehörige <form> sendet bilderscan_action=truncate
und den Bildpfad per POST. performConfig() geht dabei so vor:
findJpegRealEnd()sucht in der Datei den echten JPEG-Endmarker (EOI,0xFFD9) und liefert die Byte-Position direkt dahinter – also die Länge, die die Datei eigentlich haben sollte.truncateFile()legt zuerst überbackupOriginal()ein Backup an und kürzt die Datei anschließend perftruncate()auf genau diese Länge. Es wird kein Pixel neu kodiert – der komplette Bildinhalt bleibt byteidentisch, es wird nur der überflüssige Anhang entfernt.
Wie findJpegRealEnd() im Detail funktioniert: Ein JPEG besteht aus
einer Folge von Markern (0xFF gefolgt von einem Marker-Byte). Die
Funktion läuft die Datei durch und sucht immer das nächste "echte"
0xFF-Marker-Paar, wobei sie zwei Sonderfälle überspringt, die keine
echten Marker sind: Stopfbytes (0xFF 0x00, die innerhalb komprimierter
Bilddaten vorkommen, damit ein zufälliges 0xFF dort nicht als Marker
missverstanden wird) und Restart-Marker (0xFFD0–0xFFD7). Alle anderen
Marker haben ein 2-Byte-Längenfeld, über das die Funktion direkt zum
nächsten Marker springt – das gilt auch für den Scan-Start-Marker (SOS,
0xFFDA): dessen Längenfeld bezieht sich nur auf den kurzen Header, die
anschließenden komprimierten Bilddaten werden von der Schleife dank der
Stopfbyte-Erkennung automatisch mit-übersprungen, ohne dass die Funktion
"wissen" muss, dass sie sich gerade in Bilddaten befindet. Der erste
0xFFD9 (End of Image), auf den man so trifft, ist das echte Bildende;
alles danach in der Datei ist der Datenmüll.
Aus Performance-Gründen wird die Datei dafür komplett in den Speicher
geladen (file_get_contents) – bei Dateien über 30 MB wird die Prüfung
übersprungen (findJpegRealEnd() liefert dann null, kein Fehler). Das
ist für Blog-Fotos in der Praxis kein Problem, sollte man aber im Hinterkopf
behalten, falls mal wirklich riesige JPEGs im Spiel sind.
Zur Sicherungskopie: backupOriginal() wird sowohl von resizeImage()
als auch von truncateFile() genutzt und kopiert nur, wenn unter
_originals_backup/<dateiname> noch nichts liegt. Das ist beabsichtigt:
Führt man an derselben Datei nacheinander mehrere Aktionen aus (z. B. erst
"Bereinigen", später "Verkleinern"), bleibt im Backup immer der
allererste, unveränderte Originalzustand erhalten – nicht die bereits
bearbeitete Zwischenversion. Einzige Lücke: Wird die Backup-Datei manuell
gelöscht, legt die nächste Aktion ein neues Backup an, aber eben vom dann
aktuellen (ggf. schon veränderten) Stand.
Nach beiden Aktionen (Verkleinern wie Bereinigen) wird zusätzlich
clearstatcache(true, $path) aufgerufen. Ohne das würde PHP innerhalb
desselben Seitenaufrufs beim direkt anschließenden Scan noch die alte,
zwischengespeicherte Dateigröße melden, obwohl die Datei auf der Platte
bereits die neue, korrekte Größe hat.
Verkleinern und Bereinigen sind zwei von drei schreibenden Aktionen des Plugins – die dritte ist die Backup-Verwaltung (siehe nächster Abschnitt). Ohne einen entsprechenden Button werden nie Dateien verändert.
7. Backup-Verwaltung
Ganz unten auf der Seite gibt es einen aufklappbaren Abschnitt
„🗄 Backup-Verwaltung“ (natives HTML <details>/<summary>, kein
JavaScript nötig), der alle bisher angelegten Backups auflistet – auch
solche, deren zugehöriges Bild inzwischen nicht mehr in einem Artikel
eingebunden ist und deshalb beim normalen Scan gar nicht mehr auftauchen
würde. Pro Backup gibt es zwei Buttons:
- Zurückspielen: Kopiert die Backup-Datei zurück auf den Original-Pfad und überschreibt damit die aktuelle Datei (bzw. legt sie neu an, falls am erwarteten Pfad gerade nichts liegt). Das Backup selbst bleibt danach unverändert liegen (man kann also mehrfach zurückspielen). Das ist die einzige Aktion des Plugins, die in einen normalen (Nicht-Backup-)Ordner schreibt – dabei immer nur an genau den einen Pfad, der zu diesem spezifischen Backup gehört.
- Löschen: Entfernt die Backup-Datei endgültig (
unlink()). Bei einem nicht zugeordneten Backup (siehe unten) erscheint eine deutlich stärkere, rot hervorgehobene Sicherheitsabfrage.
Zusätzlich gibt es oberhalb der Tabelle:
- Alle zugeordneten Backups zurückspielen: wie "Zurückspielen", aber
für alle Backups, deren Original am erwarteten Pfad gefunden wurde (nur
sichtbar, wenn es mindestens ein solches gibt). Läuft über
runBulkBackupAction('bulk_restore_matched'). - Alle Backups löschen: löscht wirklich jede gefundene Backup-Datei,
unabhängig vom Zuordnungsstatus – inklusive nicht zugeordneter. Deutlich
rot hervorgehoben, mit einer expliziten Sicherheitsabfrage inklusive
Dateianzahl (und einem Hinweis, dass darunter auch nicht zugeordnete
Backups sein können), da nicht rückgängig zu machen. Läuft über
runBulkBackupAction('bulk_delete_all'). - Checkbox-Auswahl + "Auswahl löschen": Jede Zeile hat eine Checkbox,
verknüpft per HTML5-
form-Attribut mit einem eigenen kleinen Formular oberhalb der Tabelle (so sind keine verschachtelten<form>-Elemente nötig). Der Button löscht nur die angehakten Dateien in einem Rutsch (bulk_delete_selected). Ein kleines Inline-Skript zählt beim Absenden die angehakten Boxen, verweigert das Absenden bei keiner Auswahl und fragt sonst mit der genauen Anzahl nach. Jede ausgewählte Datei durchläuft dabei ganz normalresolveBackupPath()– die Mehrfachauswahl umgeht also keine der sonstigen Prüfungen, sie spart nur die Klicks/Seiten-Reloads gegenüber einzelnem Löschen.
Jede Sammel-Aktion meldet am Ende, wie viele Dateien erfolgreich verarbeitet wurden (und ggf. wie viele fehlgeschlagen sind, z. B. wegen fehlender Schreibrechte oder einer inzwischen ungültigen Auswahl).
Es gibt bewusst keine Sammelaktion, die automatisch alle nicht zugeordneten Backups löscht. Eine frühere Version dieses Plugins hatte genau das (unter dem Namen "Nicht zugeordnete Backups löschen") – mit der falschen Begründung, das seien vermutlich Fremd-Dateien und daher gefahrlos entfernbar. Das ist falsch herum gedacht: "nicht zugeordnet" heißt nur, dass am erwarteten Pfad gerade kein Original liegt – das kann eine harmlose Fremd-Datei sein, es kann aber genauso gut die letzte noch existierende Kopie eines Bildes sein, dessen Original aus einem ganz anderen Grund verschwunden ist. Eine Ein-Klick-Sammellöschung dieser Kategorie hat in der Praxis zunächst den Verdacht auf einen Bildverlust ausgelöst (der sich im konkreten Fall dann als vermutlich plugin-unabhängig herausstellte) und wurde deshalb durch die Checkbox-Auswahl ersetzt: Man sieht weiterhin auf einen Blick, welche Backups nicht zugeordnet sind (Badge + Warntext), muss aber jede einzelne Datei bewusst anhaken statt dass eine ganze Kategorie blind auf einmal verschwindet.
Woher die Liste kommt: Jedes Mal, wenn backupOriginal() einen
Backup-Ordner anlegt (oder eine bereits bestehende Datei dort vorfindet),
merkt sich registerBackupDir() diesen Ordnerpfad dauerhaft über
set_config()/get_config() – also im normalen Konfigurationsspeicher des
Plugins, newline-separiert unter dem internen Schlüssel
known_backup_dirs. Zusätzlich prüft buildImageEntry() – das ohnehin bei
jedem Bild in jedem Artikel bei jedem Seitenaufruf läuft, nicht nur
bei einer Verkleinern-/Bereinigen-Aktion – opportunistisch, ob neben dem
gerade betrachteten Bild bereits ein Backup-Ordner liegt, und registriert
ihn dann nachträglich. Das holt auch Backups nach, die schon vor
Einführung dieser Backup-Verwaltung entstanden sind, ohne dass man extra
nochmal "Verkleinern"/"Bereinigen" klicken muss. listBackupEntries()
liest beim Seitenaufruf die so aufgebaute Ordnerliste, listet darin jede
Datei auf und prüft für jede, ob am zugehörigen Original-Pfad
(dirname($backupOrdner) . '/' . $dateiname) noch eine aktuelle Datei
existiert. Ordner, die inzwischen manuell gelöscht wurden, fliegen dabei
automatisch aus der Registrierung.
Verbleibende Lücke: Ein Backup wird nur gefunden, wenn das zugehörige Bild
(egal ob die Datei noch existiert oder nicht) noch mit seinem src-Pfad in
mindestens einem Artikeltext auftaucht – denn nur dann läuft es überhaupt
durch buildImageEntry(). Wurde das <img>-Tag komplett aus dem Artikel
entfernt, bleibt das zugehörige Backup unentdeckt, bis es erneut über eine
Aktion an genau diesem Pfad berührt wird.
Annahme, die die Zuordnung "Original vorhanden/fehlt" nutzt: Für jede
Datei in einem bekannten Backup-Ordner wird angenommen, dass sie exakt so
heißt wie das Original und direkt im übergeordneten Ordner liegt
(dirname($backupOrdner) . '/' . $dateiname) – das ist exakt das Schema,
nach dem backupOriginal() selbst Backups anlegt. Liegen in einem
Backup-Ordner zusätzlich Dateien, die nicht von diesem Plugin stammen
(z. B. Reste eines älteren, anderen Skripts mit eigenem Namensschema, oder
schlicht jede andere Datei, die dort abgelegt wurde), kann diese Zuordnung
fehlschlagen bzw. – wichtiger noch – das Plugin kann eine solche
Fremd-Datei nicht von einer echten Backup-Datei unterscheiden: Sobald ein
echtes Backup in einem Ordner registriert ist, gilt der gesamte Ordner als
bekannt, und jede weitere Datei darin wird bei den Sicherheitsprüfungen
gleich behandelt (die Prüfung ist ordner-, nicht dateibasiert). Das Plugin
weist bei nicht zugeordneten Dateien über einen Hinweistext in der
Backup-Verwaltung darauf hin; eine zuverlässige automatische
Unterscheidung "echtes Backup" vs. "fremde Datei im selben Ordner" gibt es
nicht.
Sicherheit: Die Formulare senden nur einen Pfad relativ zu
serendipityPath. resolveBackupPath() löst diesen Pfad über realpath()
auf und lässt eine Aktion nur zu, wenn (a) der aufgelöste Pfad tatsächlich
innerhalb der Serendipity-Installation liegt, (b) er auf eine existierende
Datei zeigt, und (c) sein direkter Elternordner exakt einer der
tatsächlich registrierten Backup-Ordner ist (Abgleich gegen
getKnownBackupDirs(), nicht nur ein Namensvergleich – das funktioniert
dadurch auch nach einer nachträglichen Umbenennung des Ordnernamens noch
für bereits bestehende Backups weiter). Das verhindert, dass über
manipulierte Formulardaten (z. B. ../../../../etc/passwd) beliebige
Dateien außerhalb bekannter Backup-Ordner gelesen, überschrieben oder
gelöscht werden können – alle löschenden Aktionen (einzeln, "Alle
löschen", Checkbox-Auswahl) laufen ausnahmslos über diese Funktion, es
gibt keinen Code-Pfad, der unlink() außerhalb eines registrierten
Backup-Ordners aufruft.
Backup-Ordnername konfigurierbar: Über das Feld "Name des
Backup-Unterordners" lässt sich der Ordnername (Default weiterhin
_originals_backup) ändern. Der Wert wird validiert (getBackupFolderName())
und fällt bei ungültigen Eingaben (leer, enthält / oder \, oder ist .
bzw. ..) automatisch auf den sicheren Default zurück. Eine Umbenennung
wirkt sich nur auf neu angelegte Backups aus – bereits bestehende
Ordner mit dem alten Namen bleiben unverändert auf der Platte und
weiterhin über die Backup-Verwaltung erreichbar, da die Registrierung
(siehe oben) auf tatsächlichen Ordnerpfaden basiert, nicht auf dem gerade
konfigurierten Namen.
8. Zoom-Links ohne title-Attribut
Ein weiterer aufklappbarer Abschnitt „🏷️ Zoom-Links ohne
title-Attribut“ listet alle gefundenen Zoom-Links auf, deren <a>-Tag
(z. B. <a class="serendipity_image_link" href="...jpg" title="...">)
kein title-Attribut trägt. Diese Funktion ist rein informativ – im
Gegensatz zu Verkleinern/Bereinigen/Backup-Verwaltung verändert das Plugin
hier nichts automatisch, es wird kein Artikeltext geschrieben:
- Läuft als eigener, unabhängiger Scan direkt nach dem Haupt-Scan, mit
eigener SQL-Abfrage (
SELECT id, title, body, extended FROM …). Das ist bewusst vom bestehenden, bereits gut getesteten Scan oben getrennt gehalten, um dessen Logik nicht anzufassen. Als Nebeneffekt deckt dieser Scan zusätzlich dasextended-Feld (den „weiterlesen“-Teil eines Artikels) mit ab, das der Haupt-Scan nicht berücksichtigt. - Erkennung: Eine erweiterte Version des Zoom-Link-Regex aus Schritt 2
liefert zusätzlich das komplette öffnende
<a ...>-Tag. Eintitle\s*=\s*["\']-Check darauf entscheidet, ob ein title-Attribut vorhanden ist (auch ein leerestitle=""zählt als "vorhanden" – geprüft wird nur, ob das Attribut existiert, nicht ob es einen sinnvollen Wert hat). - Vorschläge aus der Mediendatenbank:
findMediaId()versucht, zumhrefdes Zoom-Links (dem Originalbild) die zugehörige ID in der Serendipity-Mediendatenbank (Tabelle{prefix}images) zu finden – über Dateiname + Endung (name/extension). Gibt es mehrere Bilder mit demselben Dateinamen (z. B. gleicher Name in verschiedenen Jahres-/Monatsordnern), wird versucht, über den Ordnernamen impath-Feld eindeutig zuzuordnen; gelingt das nicht, liefert die Funktionnull– lieber keine Zuordnung als eine falsche. Zur gefundenen ID holtgetMediaComments()aus{prefix}mediapropertiesalle nicht-leeren Werte der PropertiesCOMMENT1(Kurzbeschreibung),COMMENT2(Langbeschreibung) undALT(Alt-Text) und zeigt jeden gefundenen Wert als eigenen Vorschlag mit eigenem Kopier-Button an. Gibt es zu einer Property mehrere Zeilen (z. B. durchproperty_group/property_subgroup, etwa bei mehrsprachigen Installationen), wird nur die erste nicht-leere übernommen. - Rückfallwert: Nur wenn in der Mediendatenbank gar nichts gefunden
wurde, wird ersatzweise das
alt-Attribut aus dem<img ...>-Tag im Artikeltext selbst als einziger Vorschlag angezeigt. HTML-Entities darin (z. B.") werden überhtml_entity_decode()aufgelöst, damit sie nicht doppelt kodiert werden, wenn sie anschließend für die Anzeige (serendipity_specialchars()) und für den Kopieren-Button (json_encode()) wieder kodiert werden. - Kopieren-Button (📋): Nutzt
navigator.clipboard.writeText()über eine gemeinsame kleine JS-Funktion (bsCopy(), einmal im<script>-Block der Seite definiert). Kopiert ohne Meldung/alert(); stattdessen wird der Button-Text kurz zu einem Haken (✓) und der Button für 5 Sekunden grün eingefärbt (.bs-copied-Klasse, persetTimeoutautomatisch wieder zurückgesetzt). Der zu kopierende Text wird perjson_encode()sicher als JS-String eingebettet und zusätzlich HTML-attribut-escapt – funktioniert auch mit Anführungszeichen oder Sonderzeichen im Text. - „Artikel bearbeiten“-Link: Öffnet die Artikel-Bearbeitungsseite im
Backend mit
target="_blank"in einem neuen Tab, analog zu den Artikel-Links in der Hauptliste. So lässt sich die Liste systematisch abarbeiten: Zeile für Zeile öffnen, gewünschten Vorschlag kopieren, im Artikel dastitle-Attribut von Hand ergänzen, nächste Zeile.
Konfigurationsfelder
| Feld | Bedeutung | Default |
|---|---|---|
| Schwellenwert Thumbnails (KB) | Ab dieser Größe gilt ein Thumbnail/Einzelbild als "groß" | 300 |
| Schwellenwert Originalbilder (KB) | Ab dieser Größe gilt ein verlinktes Original als "groß" | 800 |
| Zielgröße Thumbnails (px) | Längste Kante (Breite oder Höhe, je nachdem was größer ist), auf die Thumbnails beim Verkleinern skaliert werden; die kürzere Kante wird proportional mitskaliert | 1600 |
| Zielgröße Originalbilder (px) | Längste Kante (Breite oder Höhe, je nachdem was größer ist), auf die Originale beim Verkleinern skaliert werden; die kürzere Kante wird proportional mitskaliert | 2400 |
| Thumbnail-Erkennungsmuster | Zeichenkette im Dateinamen, die ein Bild als Thumbnail kennzeichnet | serendipityThumb |
| Anzahl größte Bilder anzeigen | Länge der Top-Liste der größten Bilder; 0 blendet sie aus |
20 |
| Schwellenwert Datenmüll (KB) | Ab dieser Menge an Bytes hinter dem echten JPEG-Ende (EOI-Marker) gilt ein Bild als betroffen und der "Bereinigen"-Button erscheint | 5 |
| Name des Backup-Unterordners | Ordnername für Sicherungskopien neben jedem bearbeiteten Bild; ungültige Werte (leer, enthält //\, ./..) fallen automatisch auf den Default zurück |
_originals_backup |
Installation
- Den kompletten Ordner
serendipity_event_bilderscaninklusive desUTF-8/-Unterordners per FTP in denplugins-Ordner der Serendipity-Installation hochladen (siehe "Sprachdateien" oben – ohne den Unterordner erscheint das Plugin auf Englisch statt Deutsch). - Im Backend: Plugins konfigurieren → Neues Plugin installieren → das Plugin "Bilder-Check" suchen und installieren.
- Danach in der Plugin-Liste bei "Bilder-Check" auf Konfigurieren klicken.
Grenzen / Hinweise
- Der Scan läuft synchron bei jedem Seitenaufruf – bei sehr vielen Artikeln kann das spürbar dauern, da alle Artikeltexte per Regex durchsucht werden.
- Es werden nur lokal auflösbare Bildpfade geprüft. Bilder, die von
einer fremden Domain eingebunden sind, werden als "nicht lokal
auflösbar" übersprungen (
resolvePath()gibtnullzurück). - Die Zuordnung Thumbnail/Original funktioniert nur für das Muster
<a href="original"><img src="thumb"></a>. Andere Einbindungsarten (z. B.srcset, CSS-Hintergrundbilder) werden nicht erkannt. - Das Verkleinern nutzt die PHP-GD-Bibliothek und unterstützt nur JPEG und PNG.
unterrichtsportal.org