Den Code am Seitenende zuerst als fgsub-image-audit.php speichern.
Vorgehen:
- Datei per FTP ins WordPress-Hauptverzeichnis von
fgsub.deladen, also dorthin, wo auchwp-config.phpliegt. - Im Browser aufrufen:
https://fgsub.de/fgsub-image-audit.php - Dabei als Admin in WordPress eingeloggt sein.
- Die Datei zeigt pro Beitrag:
- Interner Link
- Externer Link – Bild auf dem Server vorhanden
- Externer Link – Bild nicht mehr auf dem Server
- Über den Button kann man eine CSV herunterladen.
Wichtig dabei:
- Die Datei ist nur für eingeloggte Admins nutzbar.
- Das Tool durchsucht aktuell Beiträge (
post), nicht Seiten. - Es prüft lokale Bilder im Upload-Ordner und erkennt auch viele verkleinerte Varianten wie
-150x150. - Wenn Bilder stark umbenannt wurden, kann ein eigentlich vorhandenes Bild trotzdem als „fehlt“ erscheinen.
Ganz wichtig: nach der Auswertung bitte wieder löschen.
Alle Bilder, die wegen möglicher Urheberechtsverletzungen umbenannt wurden, haben dem Namen umbenannt_ vorangestellt.
<?php
/**
* FGSUB image link audit
*
* Temporary admin-only report for posts:
* - Interner Link
* - Externer Link – Bild auf dem Server vorhanden
* - Externer Link – Bild nicht mehr auf dem Server
*
* Usage:
* 1) Upload this file to the WordPress root as e.g. fgsub-image-audit.php
* 2) Call it while logged in as admin:
* https://fgsub.de/fgsub-image-audit.php
* 3) Optional filters:
* https://fgsub.de/fgsub-image-audit.php?author=13
* https://fgsub.de/fgsub-image-audit.php?status=publish
* https://fgsub.de/fgsub-image-audit.php?author=13&status=publish
* 4) Delete the file again after use.
*/
define('WP_USE_THEMES', false);
require_once __DIR__ . '/wp-load.php';
if ( ! is_user_logged_in() || ! current_user_can('manage_options') ) {
http_response_code(403);
exit('403 - Nur fuer Admins');
}
@set_time_limit(300);
global $wpdb;
$home_host = wp_parse_url(home_url(), PHP_URL_HOST);
$upload = wp_upload_dir();
$upload_dir = $upload['basedir'];
$author_filter = isset($_GET['author']) ? absint($_GET['author']) : 0;
$allowed_statuses = ['publish', 'draft', 'pending', 'private', 'trash'];
$status_filter = isset($_GET['status']) ? sanitize_key($_GET['status']) : '';
if (!in_array($status_filter, $allowed_statuses, true)) {
$status_filter = '';
}
function fgsub_normalize_image_name($filename) {
$filename = basename($filename);
$filename = preg_replace('/-\d+x\d+(?=\.[a-zA-Z0-9]+$)/', '', $filename);
$filename = preg_replace('/^(umbenannt_|renamed_|copy_|kopie_)+/i', '', $filename);
return strtolower($filename);
}
function fgsub_build_local_index($base_dir) {
$index = [];
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($base_dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($it as $file) {
if (! $file->isFile()) {
continue;
}
$ext = strtolower($file->getExtension());
if (! in_array($ext, ['jpg','jpeg','png','gif','webp','avif','bmp'], true)) {
continue;
}
$real = $file->getPathname();
$name = fgsub_normalize_image_name($file->getFilename());
if (! isset($index[$name])) {
$index[$name] = [];
}
$index[$name][] = $real;
}
return $index;
}
function fgsub_status_label($status) {
switch ($status) {
case 'publish':
return '✔ veröffentlicht';
case 'draft':
return '✏ Entwurf';
case 'pending':
return '⏳ ausstehend';
case 'private':
return '🔒 privat';
case 'trash':
return '🗑 Papierkorb';
default:
return $status;
}
}
$local_index = fgsub_build_local_index($upload_dir);
$post_types = ['post'];
$placeholders = implode(',', array_fill(0, count($post_types), '%s'));
$where = "WHERE p.post_type IN ($placeholders)";
$params = $post_types;
if ($status_filter !== '') {
$where .= " AND p.post_status = %s";
$params[] = $status_filter;
} else {
$where .= " AND p.post_status IN ('publish','draft','pending','private','trash')";
}
if ($author_filter > 0) {
$where .= " AND p.post_author = %d";
$params[] = $author_filter;
}
$sql = $wpdb->prepare(
"SELECT p.ID, p.post_title, p.post_author, p.post_date, p.post_status, p.post_content, u.display_name
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->users} u ON p.post_author = u.ID
$where
ORDER BY
CASE
WHEN p.post_status = 'publish' THEN 0
WHEN p.post_status = 'private' THEN 1
WHEN p.post_status = 'pending' THEN 2
WHEN p.post_status = 'draft' THEN 3
WHEN p.post_status = 'trash' THEN 4
ELSE 5
END,
p.post_date DESC",
...$params
);
$posts = $wpdb->get_results($sql);
$rows = [];
$summary = [
'intern' => 0,
'extern_local' => 0,
'extern_missing' => 0,
'no_image' => 0,
];
foreach ($posts as $post) {
$content = (string) $post->post_content;
preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $content, $m);
$sources = array_values(array_unique($m[1] ?? []));
if (empty($sources)) {
$rows[] = [
'id' => $post->ID,
'link' => get_permalink($post->ID),
'title' => $post->post_title,
'author' => $post->display_name ? $post->display_name : ('User-ID ' . $post->post_author),
'date' => $post->post_date,
'status' => $post->post_status,
'class' => 'Kein Bild',
'details' => '',
];
$summary['no_image']++;
continue;
}
$has_internal = false;
$has_external_local = false;
$has_external_missing = false;
$details = [];
foreach ($sources as $src) {
$parts = wp_parse_url($src);
$host = strtolower($parts['host'] ?? '');
$path = $parts['path'] ?? '';
$file = basename($path);
$is_internal = false;
if (strpos($src, '/wp-content/uploads/') !== false) {
$is_internal = true;
} elseif ($host && $home_host && strtolower($host) === strtolower($home_host)) {
$is_internal = true;
}
if ($is_internal) {
$has_internal = true;
$details[] = 'intern: ' . $src;
continue;
}
if (preg_match('#^https?://#i', $src)) {
$normalized = fgsub_normalize_image_name($file);
if (!empty($local_index[$normalized])) {
$has_external_local = true;
$details[] = 'extern + lokal vorhanden: ' . $src;
} else {
$has_external_missing = true;
$details[] = 'extern + fehlt: ' . $src;
}
}
}
if ($has_external_missing) {
$class = 'Externer Link - Bild nicht mehr auf dem Server';
$summary['extern_missing']++;
} elseif ($has_external_local) {
$class = 'Externer Link - Bild auf dem Server vorhanden';
$summary['extern_local']++;
} elseif ($has_internal) {
$class = 'Interner Link';
$summary['intern']++;
} else {
$class = 'Kein Bild';
$summary['no_image']++;
}
$rows[] = [
'id' => $post->ID,
'link' => get_permalink($post->ID),
'title' => $post->post_title,
'author' => $post->display_name ? $post->display_name : ('User-ID ' . $post->post_author),
'date' => $post->post_date,
'status' => $post->post_status,
'class' => $class,
'details' => implode("\n", $details),
];
}
if (isset($_GET['csv']) && $_GET['csv'] === '1') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=fgsub-image-audit.csv');
$out = fopen('php://output', 'w');
fputcsv($out, ['ID', 'Titel', 'Link', 'Autor', 'Datum', 'Status', 'Klasse', 'Details'], ';');
foreach ($rows as $row) {
fputcsv($out, [$row['id'], $row['title'], $row['link'], $row['author'], $row['date'], fgsub_status_label($row['status']), $row['class'], $row['details']], ';');
}
fclose($out);
exit;
}
?><!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>FGSUB Bild-Audit</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; color: #222; }
h1 { margin-bottom: 8px; }
p { margin-top: 0; }
.summary { margin: 16px 0; padding: 12px; background: #f5f5f5; border: 1px solid #ddd; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 8px; vertical-align: top; text-align: left; }
th { background: #eee; position: sticky; top: 0; }
small { color: #666; }
.missing { background: #fff0f0; }
.present { background: #fffbe8; }
.internal { background: #f1fff1; }
.none { background: #fafafa; }
pre { white-space: pre-wrap; margin: 0; font-size: 12px; }
a.button { display: inline-block; padding: 8px 12px; background: #2271b1; color: #fff; text-decoration: none; border-radius: 3px; margin-right: 8px; }
code { background: #f3f3f3; padding: 2px 4px; }
.filter-box { margin: 12px 0; padding: 10px 12px; background: #fafafa; border: 1px solid #ddd; }
.filter-box label { margin-right: 8px; }
.filter-box select, .filter-box input { padding: 4px 6px; margin-right: 12px; }
</style>
</head>
<body>
<h1>FGSUB Bild-Audit</h1>
<p>Nur fuer die temporaere Analyse. Datei nach Gebrauch wieder loeschen.</p>
<p>
<a class="button" href="?csv=1<?php echo $author_filter ? '&author=' . (int) $author_filter : ''; ?><?php echo $status_filter ? '&status=' . esc_attr($status_filter) : ''; ?>">CSV herunterladen</a>
</p>
<div class="filter-box">
<form method="get">
<label for="author">Autor-ID:</label>
<input type="number" name="author" id="author" value="<?php echo $author_filter ? (int) $author_filter : ''; ?>" min="1" placeholder="alle">
<label for="status">Status:</label>
<select name="status" id="status">
<option value="">alle</option>
<option value="publish" <?php selected($status_filter, 'publish'); ?>>✔ veröffentlicht</option>
<option value="draft" <?php selected($status_filter, 'draft'); ?>>✏ Entwurf</option>
<option value="pending" <?php selected($status_filter, 'pending'); ?>>⏳ ausstehend</option>
<option value="private" <?php selected($status_filter, 'private'); ?>>🔒 privat</option>
<option value="trash" <?php selected($status_filter, 'trash'); ?>>🗑 Papierkorb</option>
</select>
<button type="submit">filtern</button>
</form>
</div>
<div class="summary">
<strong>Zusammenfassung</strong><br>
<?php if ($author_filter): ?>
Filter aktiv: Autor-ID <?php echo (int) $author_filter; ?><br>
<?php else: ?>
Filter aktiv: alle Autoren<br>
<?php endif; ?>
<?php if ($status_filter): ?>
Statusfilter: <?php echo esc_html(fgsub_status_label($status_filter)); ?><br>
<?php else: ?>
Statusfilter: alle<br>
<?php endif; ?>
Interner Link: <?php echo (int) $summary['intern']; ?><br>
Externer Link - Bild auf dem Server vorhanden: <?php echo (int) $summary['extern_local']; ?><br>
Externer Link - Bild nicht mehr auf dem Server: <?php echo (int) $summary['extern_missing']; ?><br>
Kein Bild: <?php echo (int) $summary['no_image']; ?><br><br>
Optional per URL:<br>
<code>?author=13</code><br>
<code>?status=publish</code><br>
<code>?author=13&status=publish</code>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Link</th>
<th>Titel</th>
<th>Autor</th>
<th>Datum</th>
<th>Status</th>
<th>Klasse</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row):
$class_name = 'none';
if ($row['class'] === 'Interner Link') $class_name = 'internal';
if ($row['class'] === 'Externer Link - Bild auf dem Server vorhanden') $class_name = 'present';
if ($row['class'] === 'Externer Link - Bild nicht mehr auf dem Server') $class_name = 'missing';
?>
<tr class="<?php echo esc_attr($class_name); ?>">
<td><?php echo (int) $row['id']; ?></td>
<td><a href="<?php echo esc_url($row['link']); ?>" target="_blank">öffnen</a></td>
<td><?php echo esc_html($row['title']); ?></td>
<td><?php echo esc_html($row['author']); ?></td>
<td><small><?php echo esc_html($row['date']); ?></small></td>
<td><?php echo esc_html(fgsub_status_label($row['status'])); ?></td>
<td><?php echo esc_html($row['class']); ?></td>
<td><pre><?php echo esc_html($row['details']); ?></pre></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</body>
</html>


