Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Jádro / Varianty obrázků

Varianty obrázků#

Z jednoho nahraného obrázku vzniká několik zmenšenin („variant“). Co a jak velké se vyrobí, se čte z databáze a edituje v administraci. Pro správce totéž v Varianty obrázků.

Tabulky a entity#

Migrace sql/2026-10-08_01_system_asset_variants_tables.sql (seed _02, Api RBAC _03, stránky a Admin RBAC _04; rollbacky _51–_54). Varianta jen se šířkou a sloupec usage_key přibyly migracemi sql/2026-10-09_01_system_asset_variants_height_zero.sql (změna CHECKu, rollback _51) a sql/2026-10-09_02_system_images_usage_key.sql (rollback _52) — viz Varianta jen se šířkou a Záznam obrázku.

Tabulka Co je Důležité
cms_system_asset_variants varianta = technické údaje: name, max_width, max_height (0 = bez omezení výšky), resize_mode (fit / cover), watermark, position, activated name je UNIQUE a po založení neměnný; UNIQUE také (max_width, max_height, resize_mode, watermark); CHECK chk_asset_variant_dims (max_width >= 1, max_height >= 0)
cms_system_asset_usages použití = dřívější „asset type“ (usage_key, např. blog-articles), module_id, keep_original, position zakládá jen migrace; popisek se bere z překladu admin.system.assetUsages.<usage_key>
cms_system_asset_usage_variants které varianty se pro použití vyrábějí: usage_id, variant_id, alias, position UNIQUE (usage_id, alias); FK na variantu je RESTRICT, na použití CASCADE
  • Entity: ImageVariant a ImageUsage (Shared MappedSuperclass v app/Core/Base/Shared/System/Models/Entities/ + vrstvy Api a Admin), enum ImageResizeMode (fit = NetteImage::ShrinkOnly, cover = NetteImage::Cover).
  • Přiřazení (cms_system_asset_usage_variants) není ORM entita. Čtou se i zapisují raw DBAL v AssetUsageRepository a ukládají se jednou Api operací saveAssignments — jedna transakce, FOR UPDATE na řádek použití, DELETE + INSERT. Důvod: kolekce přes ApiBridge mají známé pasti (viz Hydrátory).
  • Originál není varianta. Je to příznak keep_original u použití; složka je vždy original a alias nemá. U videa se originál ukládá vždy.
  • Pravidla (alias ^[a-z0-9-]{1,32}$, jedinečnost aliasu a složky, povinné aliasy podle použití — viz Povinné aliasy podle použití, šířka 1–8000 px, výška 0–8000 px) drží čistá funkce App\Core\Utils\Helpers\AssetVariantPlan. Admin ji použije k předběžné validaci, autoritou je Api, které validuje znovu.

Varianta jen se šířkou#

max_height = 0 znamená „bez omezení výšky“. Sloupec je smallint unsigned a UNIQUE nulu pouští, měnil se jen CHECK chk_asset_variant_dims (migrace 2026-10-09_01, opakovatelná: DROP CONSTRAINT IF EXISTS + ADD; rollback _51 smaže varianty s výškou 0 i jejich přiřazení).

  • AssetVariantPlan::validateVariant() povolí výšku 0..8000; cover s výškou 0 odmítne kódem coverNeedsHeight (formulář ho mapuje na pole maxHeight). Doporučený název: w1600.
  • VariantRenderer: maxWidth > 0 && maxHeight === 0 → resize($w, null, ShrinkOnly) (poměrově, bez zvětšení); 0×0 zůstává syrová kopie; vodoznak a kontrola obrázkové bomby beze změny.
  • ⚠️ Změna chování: obrázková knihovna (system-images) a vlastní plány s maxWidth > 0 a maxHeight = 0 se nově zmenší podle šířky (dřív syrová kopie). V assetVariants.neon takový řádek není (všech 24 má 0/0).

Vykreslení: srcset ze záznamu obrázku#

Responzivní <img srcset sizes> se skládá ze skutečných šířek v záznamu obrázku, ne z maxWidth (ten je jen horní mez a w deskriptor s nepravdivou šířkou by prohlížeč svedl).

Entita Asset (app/Core/Base/Shared/System/Models/Entities/Asset.php):

Metoda Co vrací
getVariantFiles() soubory variant bez disku: folder, variant, alias, width, height, maxWidth, original; width/height jen z meta.variants[folder], bez meta null
getVariantsSortedByWidth() zmenšeniny vzestupně podle skutečné šířky (jinak maxWidth), originál vždy poslední
getVariantFallbackOrder(?string $wanted) složky v pořadí náhrady chybějícího souboru (viz pravidla vykreslení níže); šířku neznámé složky odvodí z názvu WxH nebo wN
getThumbnailPath() cesta náhledu: thumb → nejmenší zmenšenina → originál → plná cesta; video: poster thumb jen když existují postery, jinak soubor videa

App\Core\Utils\Helpers\AssetSrcset (candidates(), build()) je čistá logika bez disku: bez originálu, jen se skutečnou šířkou, shodná šířka jen jednou (vyhraje menší maxWidth), méně než dva kandidáti = prázdný výsledek (vykreslí se prosté <img>); mezera a čárka v URL se kódují.

Front AssetRenderer ({control frontSystemAssetRenderer $asset, …}) má dva parametry:

  • responsive: true — zapne srcset, výchozí sizes="100vw";
  • sizes: '(min-width: 992px) 33vw, 100vw' — vlastní sizes, zapíná srcset i bez responsive.

Volání jen s variant: 'thumb' bez sizes (většina výpisů) se nemění: prosté <img>. Šablona dává <picture><img src srcset sizes></picture> (obal zůstává kvůli stylům šablon, <source media> zmizely); asset-fallback.js při chybě načtení odebere i srcset a sizes.

Chybějící soubor řeší AssetRenderer ve dvou krocích:

Krok Kdo Co udělá
server AssetPlaceholderHelper::exists() is_file() nad cestou odvozenou z veřejné URL; chybí-li soubor, vezme další složku podle pořadí náhrady, nakonec zástupný obrázek. Když cestu odvodit nejde, vykreslí normální <img> (existující obrázek se nikdy neschová)
klient data-asset-fallback + asset-fallback.js při error (404, zastaralé meta, soubor mimo web server) dosadí do src zástupný obrázek

Zástupný obrázek je obrázek, ne box s ikonou: prohodí se jen src, takže styly výpisu (object-fit, hover zoom, výška boxu) platí dál. Video se nenahrazuje: bez souboru se místo přehrávače vykreslí zástupný <img>. Volání {control frontSystemAssetRenderer null, alt: $name} dá zástupný obrázek rovnou (položka bez obrázku); v {foreach} volej {multicontrol …}.

Volání bez variant (většina výpisů) míří nejdřív na soubor přímo ve složce assetu, který zakládalo jen staré nahrávání; dnešní pipeline ukládá jen do složek variant. Když chybí, zkusí se zmenšeniny vzestupně a originál až jako poslední, takže do malého okénka výpisu nepoletí plná fotka.

Pravidla vykreslení:

Téma Pravidlo
Originál v srcset Originál do srcset nejde (nemá strop velikosti); bez aspoň dvou zmenšenin se známou šířkou je výstup prosté <img>.
Zapnutí srcset srcset jen na vyžádání: responsive: true nebo předané sizes.
Výchozí sizes Výchozí sizes u responsive: true je 100vw.
src src = požadovaná varianta, jinak big, jinak největší zmenšenina.
width / height Na <img> se nedávají width/height (změnilo by rozvržení výpisů; samostatný krok).
Chybějící varianta Náhrada chybějící varianty: požadovaná → nejbližší větší zmenšenina → menší sestupně → originál → zástupný obrázek.
Náhled v administraci Náhled v administraci je složka thumb, jinak nejmenší zmenšenina, jinak originál.

Varianta bez aliasu (u použití) se vyrobí do složky podle názvu a do srcset se dostane jako další šířka; povinné aliasy použití (většinou big a thumb) zůstávají povinné.

Čtení: AssetVariantProvider#

App\UI\Api\System\Models\Helpers\AssetVariantProvider::getVariants(string $type) vrací stejný tvar jako dřív, jen s několika klíči navíc:

array<string /* složka */, array{
    maxWidth: int, maxHeight: int, resizeFlags: list<int>, useWatermark: bool,
    variant?: string|null, alias?: string|null, original?: bool
}>
  • Složka = alias ?? název varianty; pořadí podle position, original vždy poslední (a jen při keep_original = 1). Neaktivní varianty se vynechají.
  • Plán se načte jedním dotazem a drží v paměti requestu (žádný Redis, žádné zneplatňování).
  • assetVariants.neon je už jen výchozí sada a fallback pro 24 použití ostatních modulů. Použije se, když tabulky chybí (kód nasazený před migrací), dotaz selže, nebo použití nemá v DB aktivní přiřazení; provider pak zapíše log assets. Změna rozměrů v neonu se na webu s migrovanou DB neprojeví. Pět použití modulu Systém (Identita webu, Provozovatel, knihovna obrázků) zálohu v neonu nemá — jejich plán čte provider jen z DB.
  • VariantRenderer už provider neinjektuje, dostává přímo %watermarkPath% (jinak by v DI vznikl cyklus).

Výjimky zanikly

Galerie a kategorie Blogu, kategorie Inzerce a Diskuze a produkt/sada v E-shopu dřív četli rozměry z nastavení modulu (variantFromSettings(), EshopAssetTrait). Nyní všechny čtou provider pod svým použitím. Do tabulek patří i Identita webu, Provozovatel a knihovna obrázků, viz Obrázky modulu Systém.

Staré klíče nastavení se už nečtou

productImage*, productThumbnail*, productResizeType*, gallery*, article* a category.image.* / category.thumb.* nečte žádný kód ani formulář. V tabulce cms_system_settings zatím zůstávají (mazací migrace nevznikla). Nevracejte je do formulářů — saveSettingForm() zapisuje všechny vyjmenované klíče.

Záznam obrázku#

Řádek cms_system_images nese ve variants u každé varianty kromě name (= složka), rozměrů, vodoznaku a resizeFlags také:

{"name":"big","maxWidth":1200,"maxHeight":900,"resizeFlags":[],"useWatermark":false,
 "variant":"1200x900","alias":"big"}

variant je název varianty, alias alias (nebo null); originál má "original": true. Skutečné rozměry souborů zůstávají v meta.variants[<složka>]. Obrázek nahraný před zavedením tabulek nemá klíč variant — čtenáři s name jako složkou fungují beze změny. U fit je resizeFlags [] (dřív u některých typů [1]; efektivní chování je stejné).

Sloupec usage_key#

cms_system_images.usage_key (varchar(64) NULL, index idx_system_images_usage_key, bez FK záměrně; migrace 2026-10-09_02, rollback _52) nese klíč použití, pod kterým byl obrázek nahrán — příprava pro dogenerování variant. Zapisuje ho AssetProcessingTrait::saveAssetVariants() z getAssetUsageKey() (výchozí hodnota = getAssetVariantType()); ImageManager ho přetěžuje a vrací klíč právě zpracovaného použití (viz Obrázky modulu Systém). V entitě Asset::$usageKey je #[ExternalReadonly] — klient ho nepřepíše. Staré záznamy mají NULL; doplní je dogenerování, viz níž.

Povinné aliasy podle použití#

Šablony žádají některé složky jménem (<head> favicony, karta provozovatele), proto povinné aliasy nejsou pro všechna použití stejné. Drží je konstanta AssetVariantPlan::REQUIRED_ALIASES_BY_USAGE a čte je AssetVariantPlan::requiredAliasesFor(string $usageKey): list<string>; použití mimo konstantu dostane REQUIRED_ALIASES (big, thumb).

Použití (usage_key) Povinné aliasy
system-identity-favicon icon32, icon180, icon512
system-identity-logo thumb
system-operator-photo avatar, thumb
system-images thumb
system-identity-og-image a všechna ostatní big, thumb

Funkci používá validateAssignments() (nepovinný 4. parametr $usageKey; prázdný = big + thumb, takže staré volání se nemění), AssetUsageManager::saveAssignments(), komponenta AssetUsageSettings (předběžná validace v adminu), AssetVariantManager::validate() a AssetVariantFormTrait (variantu s povinným aliasem některého použití nejde vypnout — chyba requiredInUse). Kód chyby missingAlias nese jméno chybějícího aliasu.

Nové použití s vlastními povinnými aliasy = přidejte řádek do konstanty, jinak se použije dvojice big + thumb.

Obrázky modulu Systém#

Identita webu, Provozovatel a knihovna obrázků (dřív varianty natvrdo ve formulářových traitech) berou plán z tabulek stejně jako ostatní použití. Migrace sql/2026-10-17_01_system_asset_variants_system_usages.sql (rollback _51_…_rollback.sql; opakovatelná, bez DELIMITER, deploy ji pustí sám) založí pět použití modulu System (module_id 1), chybějící varianty (dedup podle rozměrů, režimu a vodoznaku), přiřazení s NOT EXISTS a doplní usage_key existujícím obrázkům.

usage_key Popisek v administraci Výchozí přiřazení (alias: rozměr)
system-identity-favicon Identita webu — favicona icon32: 32×32, icon180: 180×180, icon512: 512×512 (cover) + originál
system-identity-og-image Identita webu — sdílecí obrázek big: 1200×630 (cover), thumb: 400×210 (cover) + originál
system-identity-logo Identita webu — logo thumb: 400×200 (fit) + originál
system-operator-photo Provozovatel — fotka avatar: 240×240 (cover), thumb: 400×400 (fit) + originál
system-images Knihovna obrázků big: 1200×900, thumb: 400×300 (fit) + originál

Logo = všechna obrazová pole Identity kromě favicony a sdílecího obrázku.

Plán určuje Api, ne formulář. Admin traity (SiteIdentityFormTrait, OperatorFormTrait, ImageFormTrait) neposílají varianty; do $meta dají jen usageKey:

$meta['usageKey'] = 'system-operator-photo';

App\UI\Api\System\Models\Managers\ImageManager přijme jen klíče z whitelistu USAGE_KEYS (těch pět výše); jiný, chybějící nebo nestringový klíč spadne na system-images, takže si klient Api nemůže vymyslet cizí použití. Plán vezme z AssetVariantProvider a klíč zapíše do cms_system_images.usage_key; při výměně souboru se usage_key kopíruje na editovaný záznam spolu s variants. Chybí-li plán v DB (pro těchto pět použití není záloha v neonu), vrátí Result::failure(ResultCode::NOT_AVAILABLE) místo obrázku bez složek. Složku original už Api nevynucuje — řídí ji keep_original použití jako u ostatních. $meta teče Admin → Api bez filtru (interní transport); na externí cestě ho filterExternalMeta() zahodí.

Bez zálohy v neonu. config/Shared/assetVariants.neon těchto pět typů neobsahuje — jediným zdrojem pravdy jsou tabulky variant v DB. Když plán chybí (výpadek tabulek, použití bez přiřazených variant), nahrání se odmítne: administrace (Identita webu, Provozovatel, Obrázky) ukáže srozumitelnou hlášku, nic se neuloží a dosavadní obrázek zůstane.

Knihovna obrázků. Zadávání variant u jednotlivého obrázku (Multiplier) skončilo. Nové nahrání bere plán použití system-images; editace (Image/edit.latte) ukazuje čtecí přehled variant z Asset::getVariantFiles() a uložení bez nového souboru záznam variants nemění.

Kontrakt. php tests/Manual/System/asset_variants_system_contract.php porovná efektivní plán pěti použití s hodnotami natvrdo (tests/Fixtures/asset-variants-system-before.json), ověří povinné aliasy a whitelist usageKey; výstup 5/5 OK, jinak diff a exit 1. Snímek 24 původních typů hlídá dál asset_variants_contract.php. Jednotkové testy povinných aliasů: tests/Unit/System/AssetVariantPlanTest.php.

Administrace a RBAC#

  • Obrazovka Nastavení → Varianty obrázků: AssetVariantPresenter (výpis, přidání, úprava, mazání signálem deleteVariant! (POST, #[Requires(methods: 'POST')]; řádková akce gridu má ->setPost(), tlačítko se odešle prázdným formulářem mimo gridForm), záložka Použití).
  • Přiřazení v nastavení modulů (Blog, E-shop, Inzerce, Katalog firem, Diskuze): komponenta AssetUsageSettings (app/UI/Admin/System/Components/AssetUsageSettingsComponent/) přidaná traitem App\Core\Traits\Admin\Presenters\AssetUsageSettingsTrait; má vlastní formulář a tlačítko, neukládá se s formulářem nastavení.
  • Záložka Použití staví jednu komponentu na modul: AssetVariantPresenter::createComponent() vytváří assetUsageSettings_<moduleId> a AssetUsageSettings::setSection() řídí titulek nad prvním. Jeden společný formulář by měl kolem 1700 polí (29 použití × 19 variant × 3 pole) a překročil by PHP max_input_vars (výchozí 1000) — odeslání by se tiše zahodilo. Největší formulář je teď kolem 420 polí; nové použití s mnoha variantami hlídejte.
  • Api presentery api/system/asset-variant (CRUD) a api/system/asset-usage (get-assignments, save-assignments, get-regeneration-stats, regenerate-variants, delete-originals, delete-outdated-variants).
Vrstva Klíče
Admin :Admin:System:AssetVariant:show, :add, :edit, :del, :regenerate, :deleteOriginals, :deleteOutdated
Api — varianty :Api:System:AssetVariant:get, getAll, getBy, save, delete, updateColumns, deleteAll
Api — přiřazení :Api:System:AssetUsage:getAssignments, :saveAssignments, :getRegenerationStats, :regenerateVariants, :deleteOriginals, :deleteOutdatedVariants

Přiřazení v nastavení modulu vyžaduje :Admin:<Modul>:Setting:show; záložka Použití (vč. modulu Firma) :Admin:System:AssetVariant:edit. Api deleteAll varianty je odmítnuté (obcházelo by kontrolu použití), updateColumns se validuje stejně jako uložení.

Dogenerování a mazání originálů#

Staré obrázky mají jen složky, které plán použití předepisoval při nahrání. Dogenerování je doplní a umí smazat uložené originály. Migrace sql/2026-10-10_01 (sloupec variants_attempts), _02 (Api RBAC) a _03 (Admin RBAC); mazání variant mimo nastavení: sql/2026-10-13_01 (Api RBAC deleteOutdatedVariants) a _02 (Admin RBAC deleteOutdated); rollbacky 2026-10-10_51–_53, 2026-10-13_51–_52; všechny opakovatelné, deploy je pustí sám. Pro správce: Staré obrázky: dogenerování.

Část Co dělá
App\Core\Utils\Helpers\AssetVariantBackfill čistá logika bez disku a DB: decide() (plán × záznam × soubory na disku → create / reconcile / skipped + důvod tooLarge, noSource, watermarkedSource; stav complete / pending / blocked / exhausted), classify() (tři počty u použití), expectedSize() / staleFolders() / withOverwrite() (přegenerování existujících), outdatedFolders() / withoutFolders() / canDeleteOutdated() (varianty mimo nastavení), applyToRecord(), canDeleteOriginal(), withoutOriginal(), usageKeyFromPath(), likePatterns(); MAX_ATTEMPTS = 3, PROTECTED_FOLDERS
App\UI\Api\System\Models\Managers\AssetVariantRegenerationManager procesní orchestrátor mimo Services/: findStats(), regenerate(?usageId, batch, overwrite, retry), deleteOriginals(usageId), deleteOutdatedVariants(usageId); řádky obrázků jdou raw DBAL přes řetěz AssetManager → AssetService → AssetMapper → AssetRepository, nové složky vyrábí týž VariantRenderer jako nahrání
AssetUsagePresenter (Api) get-regeneration-stats, regenerate-variants (overwrite, retry), delete-originals, delete-outdated-variants; zapisující jen POST (GET = 405), usageId a příznaky přes RequestParamShape (špatná hodnota = 400)
AssetVariantRegeneratePresenter (Cron) /cron/system/asset-variant-regenerate?token=…[&limit=]
AssetVariantPresenter (Admin) signály handleRegenerate(usageId) (pole POST overwrite, retry), handleDeleteOriginals(usageId) a handleDeleteOutdated(usageId) na záložce Použití, všechny #[Requires(methods: 'POST')] a vlastní brána

Cron#

/cron/system/asset-variant-regenerate?token=…[&limit=25]: nejdřív doplní usage_key starým obrázkům, pak zpracuje nejvýš limit obrázků na každé použití (výchozí DEFAULT_BATCH = 25, strop MAX_BATCH = 500). Odpověď nese počty (processed, created, overwritten, failed, skipped, remaining). Existující soubory cron nepřepisuje (přepsání existujících je jen akce u jednoho použití v administraci, resp. Api regenerate-variants s overwrite). Po nasazení ho opakujte, dokud remaining není 0; pak stačí denně v noci. Frekvence je v docblocku presenteru, takže se objeví v přehledu naplánovaných úloh.

Zámek a dávky#

  • Zámek CronLockManager se jménem asset.variants.regenerate (TTL 300 s, po každém obrázku se prodlužuje) sdílí cron, tlačítko i mazání originálů, takže dávky nejedou souběžně. Druhá strana dostane DB_LOCKED a administrace napíše „už běží".
  • Rozpočet času jedné dávky je 45 s; mazání originálů zpracuje nejvýš 500 souborů na kliknutí (DELETE_BATCH), zbytek vrátí jako remaining.

Pokusy: variants_attempts#

Sloupec cms_system_images.variants_attempts (tinyint unsigned, #[ExternalReadonly]). Neúspěch (nečitelný zdroj, výjimka při vykreslení) ho zvýší; po 3 pokusech je obrázek exhausted a přeskakuje se. Úspěšné zpracování obrázku ho vynuluje (saveVariantRecord(…, resetAttempts: true)), „nejde dogenerovat" ho nemění. Tlačítko Zkusit znovu (regenerate-variants s retry) pokusy použití nejdřív vynuluje (resetVariantsAttempts()), pak spustí dávku. attempts > 0 je sám o sobě stav „Nepodařilo se vygenerovat". Vlastní sloupec je záměr: thumbnails_attempts patří catch-up cronu videí a u obrázků ho zvyšuje každé nahrání.

Doplnění usage_key z cesty#

Staré záznamy mají usage_key = NULL. Každý běh dávky je doplní jedním UPDATE … CASE WHEN path LIKE … podle cesty (AssetVariantBackfill::PATH_USAGE, kořeny assetDir z parameters.neon). Co se nedá určit jednoznačně, zůstane NULL a přeskočí se; dohad se nezapisuje. ⚠️ Nové použití s vlastním kořenem patří i do PATH_USAGE, jinak jeho staré obrázky zůstanou bez klíče.

Pravidla dogenerování a mazání#

Téma Pravidlo
Zdroj Zdroj dogenerování je originál. Bez něj se z největší existující zmenšeniny bez vodoznaku vyrobí jen varianty s menší nebo stejnou šířkou (výšku neřeší); zdroj s vodoznakem = blocked (watermarkedSource).
Použití obrázku Použití obrázku = usage_key; u NULL se doplní z cesty, nejednoznačné se přeskočí.
Doplňování Jen doplňuje chybějící složky (chybí = soubor není na disku). Existující soubor se nepřepisuje. Složka, jejíž soubor na disku je a záznam ji nezná (přerušený běh), i neznámý original/ se jen zapíše do záznamu (reconcile) spolu s meta z getimagesize. Video a SVG se přeskakují.
Dávka Dávka 25 obrázků na použití; GET_LOCK, 3 pokusy.
Smazat originály „Smazat originály": jen rastr, jen když záznam i disk mají original a big. Nejdřív se zapíše záznam (bez original ve variants i meta), pak se smaže soubor. Použití s keep_original = 1 Api odmítne (INVALID_STATE), UI tlačítko neukáže. Vlastní klíč, jen POST.
Počty u použití Tři čísla u použití — počty obrázků, obrázek může být ve více: nemá aktuální varianty (missing: plán má složku, kterou záznam nezná, soubor není na disku a jde vyrobit), nepodařilo se vygenerovat (failed: záznam složku uvádí a soubor chybí, nebo variants_attempts > 0), nelze dogenerovat (unfixable: soubor chybí a není z čeho — bez originálu je cíl větší než největší zmenšenina bez vodoznaku). Jen chybějící zápis o existujícím souboru se nepočítá nikam. Čistá funkce AssetVariantBackfill::classify().
Přegenerovat i existující „Přegenerovat i existující" (overwrite): přepíše soubory plánových složek, jejichž skutečné rozměry (z meta, jinak getimagesize) neodpovídají tomu, co by z originálu vzniklo dnes (expectedSize() počítá stejně jako VariantRenderer — Nette Image::calculateSize(), vč. Cover); jen s existujícím originálem, respektuje limit pokusů. Zápis atomicky: render do .regen-<náhodně>-<jméno>.<přípona> ve stejné složce, po vyrenderování všech rename(), finally uklidí dočasné soubory. Aktualizuje variants i meta. Vodoznak se nezkoumá.
Varianty mimo nastavení „Smazat varianty mimo aktuální nastavení": složky, které záznam má a plán použití ne. Nikdy big / thumb / original — chrání se složka i alias (PROTECTED_FOLDERS). Jen rastr a jen s originálem na disku; plán bez zmenšeniny nic neuklízí. Nejdřív záznam (variants i meta přes withoutFolders()), pak soubor + rmdir prázdné složky (sdílí ji ostatní obrázky cesty). Nejvýš 500 obrázků na kliknutí. Vlastní Api i Admin klíč, jen POST.

RBAC#

Vrstva Klíče
Api :Api:System:AssetUsage:getRegenerationStats, :regenerateVariants, :deleteOriginals, :deleteOutdatedVariants (skupiny System:AssetUsage:show, regenerate, deleteOriginals, deleteOutdatedVariants; jen role admin)
Admin :Admin:System:AssetVariant:regenerate, :deleteOriginals, :deleteOutdated (vlastní klíč pro nevratné mazání)

Po nasazení smažte temp/cache (RBAC). Ověřte, že GET na signály dává 405 a že NonstandardActionPrivilegeGateTest hlídá bránu i #[Requires(methods: 'POST')].

Formáty nahrávaných souborů#

Které formáty jde nahrát, ví jediné místo: AssetFormatRegistry. Sbírá přípony a MIME typy z procesorů (AssetFormatProcessor), každý procesor obslouží jeden druh média (AssetFormatKind). Volající nikdy neuvádí přípony, ale druhy, které v daném místě připouští.

Procesor Druh Přípony Co udělá
RasterImageProcessor Raster jpg, jpeg, png, gif, webp (jfif, jpe, jif se přejmenují na jpg) ověření ImageUploadPolicy, varianty přes VariantRenderer (resize, vodoznak)
VectorImageProcessor Vector svg očištění SvgSanitizer, pak tytéž bajty do každé varianty
VideoProcessor Video mp4, mov ověření obsahu přes ffprobe, originál do original/, varianty jako poster z prvního snímku

Co kde platí, je v kódu managerů ($kinds před voláním saveAssetVariants()): obrázky Blogu, Company a e-shopu berou jen Raster, ikony kategorií Inzerce a Katalogu firem Raster + Vector, administrace „Obrázky“ (ImageManager) všechny tři. Bez zadání bere AssetProcessingTrait jen Raster (saveAssetVariants(), resolveAssetExtension()); galerie z dropzone bere getAssetFormatKinds(), výchozí Raster + Video.

Varianta bez transformace (maxWidth i maxHeight 0, bez vodoznaku) se zapíše jako syrová kopie bajtů, ne přes GD. Až když nějaká varianta potřebuje dekódování, ověří se rozměry z hlavičky proti stropu ImageUploadPolicy. Malý soubor s obrovskými rozměry tak neprojde do GD a nevyčerpá paměť.

Postup: přidávám nový formát

  1. Napiš třídu implementující AssetFormatProcessor: getExtensions(), getMimeTypes(), getKind() a process(), který jen zapíše soubory a vrátí AssetProcessingResult.
  2. Zaregistruj ji jako službu v config/Shared/assetVariants.neon a přidej ji do processors služby assets.formatRegistry. Jinde se nic neupravuje.
  3. U managerů, kde má formát projít, doplň jeho druh do $kinds. U SVG navíc formulář administrace potřebuje addImageUpload(…, allowVector: true), jinak soubor odmítne už validace pole.
  4. Smaž temp/cache (DI kontejner) a nahraj soubor na každém místě, kde má projít, i tam, kde projít nemá.

Procesor nezakládá Asset ani nesahá do databáze

Řádek obrázku zakládá a ukládá volající v save transakci. Procesor, který zapíše do DB sám, obejde rollback: neúspěšné uložení po sobě nechá řádek, který na nic nepatří.

Dva procesory se stejnou příponou shodí kontejner

Registr při sestavení vyhodí LogicException a padne každá stránka, která nahrávání používá. Přípony musí být mezi procesory disjunktní.

Neznámá přípona se nepřejmenovává

resolveExtension() vrátí null a volající soubor zahodí. Nezkoušej neznámou příponu „zachránit“ přejmenováním na .jpg: SVG by se tak uložilo jako .jpg se značkami uvnitř.

SVG#

SVG je XML, které umí spustit skript. Soubory se servírují přímo z www/, takže SVG otevřené na vlastní adrese běží jako dokument na doméně, kde má administrátor session. Proto ho SvgSanitizer čistí whitelistem:

Pravidlo Proč
element a atribut mimo ALLOWED_ELEMENTS / ALLOWED_ATTRIBUTES zmizí, on* vždy blacklist vždy na něco zapomene
<image> a <a> v seznamu nejsou ikona je nepotřebuje a oba umí odkázat ven
href / xlink:href jen #kotva <use href="#icon"> funguje, odkaz ven ne
maska DANGEROUS_CSS (url() mimo #, @import, expression(, javascript: …) běží na každý atribut i na <style> prezentační atributy (fill, filter) berou url() stejně jako CSS
před maskou se rozbalí CSS escapy a vypustí komentáře @\69 mport čte prohlížeč jako @import
neplatné XML, kořen jiný než <svg> nebo vnitřní DTD = odmítnuto entitní bomby a XXE

Nebezpečný atribut se odstraní sám, ne celý element: ikona se pak vykreslí s výchozí výplní, místo aby přišla o tvar. url(#grad) zůstává, exportované ikony ho běžně používají.

SVG se nezmenšuje, ale do každé složky varianty se zkopíruje (VariantRenderer::copyToVariants()): plán variant se do Asset.variants ukládá vždy a šablony z něj skládají thumb/<jméno>.svg.

Vector jen tam, kde nahrává administrátor

Sanitizace je obrana, ne povolení. Kontext, do kterého nahrává uživatel z frontu (přílohy, fotky inzerátů), Vector nedostává: obětí uloženého skriptu by byl administrátor, který soubor otevře.

Video#

Ověření obsahu není best-effort: soubor bez hlavičky ISO BMFF, server bez ffprobe nebo obsah, který ffprobe nepřijme jako MP4/MOV s video stopou, se odmítne a na disk se nic nezapíše. Ověřuje se v dočasném souboru mimo webroot a teprve ověřené video se přesune na místo.

Poster je naopak best-effort. Bez ffmpeg nebo při chybě extrakce obrázek vznikne s thumbnailsGenerated = false a varianty doplní cron /cron/system/asset-thumbnail-catchup/generate (denně ve 4:00) přes AssetManager::backfillThumbnails(). Volá tentýž VideoProcessor::renderPosterVariants() jako nahrání. Bez ffmpeg cron skončí skipped.

Po dvou neúspěšných pokusech cron video vzdá

Každý neúspěch zvýší thumbnails_attempts (i chybějící soubor nebo chybějící plán variant), a co dosáhlo AssetManager::MAX_ATTEMPTS (2), z výběru vypadne natrvalo. Po opravě serveru (doinstalovaný ffmpeg) proto vynuluj thumbnails_attempts u videí s thumbnails_generated = 0, jinak zůstanou bez posteru.

Uvolnění starého obrázku#

Řádek cms_system_images i soubory obrázku po výměně, odebrání nebo smazání majitele nezmizí samy. Uvolňuje je AssetProcessingTrait a kde to dělá, záleží na situaci:

Situace Volání Kdy běží
výměna nebo odebrání jednoho obrázku (logo, foto) releaseAsset($stary, $novyId) v afterSave, uvnitř transakce, až po úspěšném updateColumn() cizího klíče
obrázky vypadlé z galerie nebo formuláře releaseDroppedAssets($predtim, $ponechane) v afterSave, po zápisu relací a hlavního obrázku
smazání entity přes delete() completedAssetCleanup() po commitu, sám z DeleteTrait
smazání podstromu přes deleteNode() captureOwnedAssetIds() před mazáním, releaseOwnedAssets() po něm sám z TreeTrait
měkké smazání (deleted = 1) nic záznam jde vrátit, obrázek musí zůstat

Výměna běží v transakci schválně: smazání řádku je atomické s přepnutím cizího klíče a kontrola vazeb běží na stejném spojení, takže vidí právě zapsaný klíč. Smazání entity naopak běží až po commitu, protože uvnitř transakce by cizí klíč na obrázek ještě žil.

O tom, jestli se obrázek smí smazat, nerozhoduje trait, ale AssetManager::releaseAssetRow(). Vazby na obrázek hledá přes information_schema (všechny sloupce s cizím klíčem na cms_system_images), a když na obrázek ještě něco ukazuje nebo si není jistý, vrátí null a obrázek zůstane i se soubory.

Postup: manager s obrázky, které se mají uvolnit po smazání

  1. Manager používá AssetProcessingTrait.
  2. Definuj ownedAssetIds(object $entity): array: vrať cizí klíče obrázků, které entita vlastní (id, entitu Asset nebo null). Vzor Blog\…\NewsManager::ownedAssetIds().
  3. Nic dalšího. DeleteTrait i TreeTrait se podle téhle metody rozjedou samy. Výjimkou jsou obrázky ve vazební tabulce (galerie, úvodní panel), viz varování níž.
  4. Ověř: smaž záznam a zkontroluj, že řádek v cms_system_images i soubory zmizely, a že sdílený obrázek zůstal.

Obrázky z vazební tabulky zachyť v beforeDelete()

ownedAssetIds() běží až po commitu, kdy kaskáda (cms_mod_blog_gallery_image_relations, cms_mod_blog_intro_image_relations) vazby už smazala. Přednačtená entita z DeleteTrait kolekci vazeb nenese: je líná a UoW se po čtení čistí. beforeDelete() proto entitu načte znovu s includem (find($id, ['assetRels'])), id obrázků uloží do vlastnosti manageru a ownedAssetIds() je odtud jen vrátí (Blog\…\GalleryManager, IntroManager). Když ownedAssetIds() čte kolekci z předané entity, vrátí prázdno a řádky i soubory všech obrázků zůstanou osiřelé.

releaseAsset() až po úspěšném zápisu cizího klíče

Zavolaný dřív smaže obrázek, na který záznam pořád ukazuje: když potom cokoli selže, databáze odkazuje na řádek, který už neexistuje. Ze stejného důvodu kanály s jedním obrázkem staré soubory jen naskenují (listAssetFilesIn()) a smažou je až po úspěchu (deleteAssetFiles()).

releaseAsset() vrátí false u sdíleného obrázku

Pak nemaž ani staré soubory ze složky ($stalePaths): patří obrázku, na který ukazuje jiný záznam.

Rollback o souborech neví

Varianty nového obrázku se zapisují na disk uvnitř transakce. Když uložení skončí neúspěchem, smaž na neúspěšné cestě, co jsi zapsal: deleteAssetFiles($this->assetVariantFilePaths($absBase, $fileName, $variantConfigs)). Hledá se podle náhodného fileName, cizí obrázek ve stejné složce se nezasáhne.

Úklid je fail-soft: chyba se jen zapíše do logu assets a uložení ani smazání nespadne. Osiřelý řádek je menší vada než rozbitá editace.

Limit velikosti nahrávaného obrázku#

App\Core\Security\Upload\ImageUploadSizeLimit::bytes() určuje horní mez pro nahrávač obrázků inzerce a katalogu firem (front i admin): maxSize parametru nahrávače (MB) > nastavení modulu maxImageSize (MB) > 50 MB. Nahrávač souborů zůstává beze změny.

Nastavení cookies