Keep technology logos and images inside text safe from pruning
Media::referencedPaths() is what `content:prune --media` asks before deleting a file, and it did not know about two kinds of image a page shows: - A technology's logo. Every logo on the radar looked orphaned, and one prune would have deleted all of them. - Images inside rich text — a page's body, a project's description, decisions and postmortem — whether attached in the panel's editor or uploaded through the API and written in as <img src>. They are now found in the HTML, in every language, bin included. The page body's editor also states where attachments go — the media disk, pages/content, public — instead of relying on Filament's defaults: the HTML reaches the frontend as it is, and a private file would be a broken image there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -183,6 +183,7 @@ class Media extends Model
|
||||
[Member::class, 'avatar'],
|
||||
[Client::class, 'logo'],
|
||||
[Testimonial::class, 'avatar'],
|
||||
[Technology::class, 'logo'],
|
||||
[ImageAttachment::class, 'path'],
|
||||
];
|
||||
|
||||
@@ -198,9 +199,52 @@ class Media extends Model
|
||||
$paths = [...$paths, ...$query->pluck($column)->all()];
|
||||
}
|
||||
|
||||
// Images placed inside rich text — attached in the panel's editor, or
|
||||
// uploaded through the API and written into a body as `<img src>`. No
|
||||
// column holds their path, so without this they would look orphaned
|
||||
// and `content:prune --media` would delete them from under the page.
|
||||
$richText = [
|
||||
[Page::class, ['body']],
|
||||
[Project::class, ['description', 'decisions', 'postmortem']],
|
||||
];
|
||||
|
||||
foreach ($richText as [$model, $columns]) {
|
||||
$model::query()
|
||||
->withTrashed()
|
||||
->select(['id', ...$columns])
|
||||
->chunkById(200, function ($records) use ($columns, &$paths): void {
|
||||
foreach ($records as $record) {
|
||||
foreach ($columns as $column) {
|
||||
foreach ($record->getTranslations($column) as $html) {
|
||||
$paths = [...$paths, ...static::pathsEmbeddedIn((string) $html)];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($paths)));
|
||||
}
|
||||
|
||||
/**
|
||||
* The stored paths of every image on the media disk a piece of HTML links
|
||||
* to, whether written with the disk's absolute URL or its path alone.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function pathsEmbeddedIn(string $html): array
|
||||
{
|
||||
$base = (string) parse_url(Storage::disk((string) config('media.disk', 'public'))->url(''), PHP_URL_PATH);
|
||||
|
||||
if ($base === '' || ! str_contains($html, $base)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
preg_match_all('#'.preg_quote(rtrim($base, '/'), '#').'/([^"\'\s?\#<>]+)#', $html, $matches);
|
||||
|
||||
return array_map('rawurldecode', $matches[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any record still points at this file.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user