Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Jádro / Externí připojení

Externí připojení#

Služby třetích stran (reCAPTCHA, Google Analytics, Tag Manager, Search Console, Maps), jejichž klíče si provozovatel zapíná a vyplňuje v administraci — bez úpravy konfigurace a bez mazání cache. Pro provozovatele je totéž v Napojení třetích stran. Jádro: app/Core/Integrations/Connections/.

Model#

Připojení vzniká instalací, ne klikem v administraci: řádek v tabulce cms_system_external_connections se zapíše SQL skriptem. Tabulka má jen čtyři věci:

Sloupec Význam
type (UNIQUE) kód typu (recaptcha_v3, google_maps, …)
enabled zapnuto = web připojení používá
config JSON objekt klíč => skalár\|null; obsahuje tajné klíče
insert_datetime, update_datetime časy

Která pole typ má, neříká databáze, ale kód: registr typů. Uložený JSON nese jen hodnoty.

Třída Úkol
ConnectionTypeInterface getCode(), getGroup(), getFields(), supportsCheck(), check()
ConnectionField name, kind, required, public
ConnectionFieldKind Text, Secret, Bool, Number
ConnectionTypeRegistry typy podle kódu; normalize() zahodí nedeklarované klíče a přetypuje
ExternalConnections běhové čtení: isEnabled(), values(), value(), publicValues(), reset()
GoogleIntegrations hodnoty Google služeb tak, jak je web používá
  • Skupina (getGroup()) označuje alternativy: připojení téže skupiny se vzájemně vypínají (captcha = reCAPTCHA v2 × v3). null = nezávislé připojení.
  • public u pole říká, že hodnota smí do prohlížeče (vkládá se do stránky). Tajné pole (Secret) je veřejné nikdy — ConnectionField::isPublic() to vynucuje.
  • supportsCheck() rozhoduje, zda administrace nabídne tlačítko Ověřit připojení. Dnes u žádného typu: od roku 2025 Google siteverify nerozliší špatný tajný klíč bez skutečného tokenu z prohlížeče (vrací vždy invalid-input-response), takže test bez prohlížeče nic nedokazuje.

Běhové čtení#

ExternalConnections čte řádky přes QueryCache::rememberTagged() s tagem ExternalConnection; uložení v administraci tag zvedne, takže změna platí hned. V rámci jednoho požadavku drží výsledek v paměti; po zápisu ve stejném požadavku se volá reset().

Chybějící tabulka nesmí shodit web

Když se řádky nedají přečíst (migrace ještě neproběhla), logu jde varování EXTERNAL_CONNECTIONS_READ_ERROR a každé připojení se tváří jako nenainstalované. Spotřebitelé se vrátí k výchozímu chování. Poškozený záznam cache zapnuté připojení nevyrobí.

Cache vyžaduje službu v enabledServices

Aby se řádky cachovaly, musí být App\UI\Api\System\Models\Services\ExternalConnectionService v cache.config.enabledServices v local.neon serveru. Bez toho aplikace funguje, ale udělá jeden dotaz na každý požadavek. Totéž platí pro SettingService. Viz Cache.

Spotřebitelé#

Typ Kdo čte Jak
recaptcha_v2, recaptcha_v3 ReCaptchaRuntime viz níž
google_analytics, google_tag_manager, google_search_console analytics.latte všech 18 šablon $googleIntegrations z Front BasePresenteru, GoogleIntegrations::forPageHead()
google_maps komponenta GoogleMap (přes továrnu) a GoogleGeocoder GoogleIntegrations::mapsKey(), mapId(); geokódování čte geocodingKey() (klíč pro server, jinak klíč stránek), viz Google Maps

GoogleIntegrations vrátí hodnotu jen u zapnutého a vyplněného připojení, jinak null (služba vypnutá). Šablony analytics.latte vkládají měřicí skripty jako type="text/plain"; spustí je až cookie-bar.js po souhlasu s kategorií cookies.

Původ hodnot

Do října 2026 žily Google hodnoty v cms_system_settings (modul 1: googleAnalyticsId, googleTagManagerId, googleSiteVerification, googleMapsKey). Přestěhovaly se do připojení a ze Nastavení webu zmizely.

Kategorie souhlasu s cookies#

Které kategorie cookie lišta nabízí, určuje jediný soubor config/Shared/cookieConsent.neon, čte ho služba App\Core\Cookie\CookieConsentConfig. Z téhož seznamu staví přepínače lišta na frontu (CookieBar) i nabídka „Souhlas s cookies“ u vlastního kódu v administraci (selectableKeys() — jen kategorie, které návštěvník může vypnout).

Klíč Význam
categories {key, readonly}; pořadí = pořadí přepínačů. readonly: true = udělená vždy, přepínač zamčený, v administraci se nenabízí
revision verze souhlasu; zvýšení zneplatní všechny udělené souhlasy a lišta se zobrazí znovu
expirationDays platnost souhlasu ve dnech (výchozí 182)

Postup: přidat kategorii

  1. Přidej {key: <klíč>, readonly: false} do categories.
  2. Doplň překlady front.base.components.cookieBar.categories.<klíč>.title a .description ve všech jazycích (app/Locale/base/<locale>/front.<LOCALE>.neon).
  3. Má-li se kategorie mapovat na Google Consent Mode nebo při odvolání mazat cookies, doplň CONSENT_MODE_SIGNALS a CATEGORY_COOKIES v www/themes/frontend/<šablona>/desktop/assets/js/components/cookie-bar.js, rozešli změnu do všech šablon (bin/theme-js-sync.php) a spusť webpack.
  4. Zvyš revision.

Bez zvýšení revision nová kategorie zůstane mlčky zakázaná

Dřívější souhlas se na ni nevztahuje. Kdo už jednou klikl, lištu znovu neuvidí a skript čekající na novou kategorii se mu nikdy nespustí.

Kategorie proto nejsou v databázi ani v administraci

Kategorie zapnutá bez překladů a bez obsluhy v cookie-bar.js by návštěvníkovi slíbila volbu, kterou nic neimplementuje: přepínač jde vypnout a nic se nestane.

Google Maps: klíče a varianty komponenty#

Připojení google_maps (GoogleMapsConnection) má tři pole:

Pole Druh Přístup
apiKey text, povinné, public GoogleIntegrations::mapsKey() — klíč pro stránky, vidí ho návštěvník
mapId text, nepovinné, public GoogleIntegrations::mapId() — jen když je i klíč stránek
serverApiKey tajné, nepovinné GoogleIntegrations::geocodingKey() — serverApiKey, jinak mapsKey(); nikdy se nepíše do stránky

Všechny tři přístupové metody vrátí hodnotu jen u zapnutého připojení s klíčem stránek; samotný serverApiKey bez apiKey proto nic neotevře. Proč dva klíče, viz Mapy a dohledání adres: webová služba Geocoding odmítá klíč omezený na domény.

Varianty komponenty GoogleMap#

GoogleMap::render($latitude, $longitude, $info, $style = GoogleMap::STYLE_EMBED):

Konstanta Šablona Co dělá
STYLE_EMBED (výchozí) Style_2.latte <iframe> s Maps Embed API a odkaz „Otevřít v Google Mapách“; nenačítá Maps JavaScript API ani googlemap.js
STYLE_DYNAMIC Style_1.latte <div> s atributy data-latitude, data-longitude, data-info, data-api-key, data-map-id, který oživí googlemap.js

Bez souřadnic se nevykreslí nic. Varianta embed se navíc nevykreslí bez klíče stránek. Dynamická mapa se volá výslovně čtvrtým argumentem: {control frontSystemGoogleMap $lat, $lng, $info, 'Style_1'}. Žádná šablona ji dnes nevolá — všech 22 míst používá výchozí vloženou mapu.

Vkládej mapu, kde stačí ukázat jedno místo

Vložená mapa nemá počítaná načtení, zatímco dynamická má u Googlu měsíční bezplatný limit (viz ceník Google Maps Platform). Dynamickou volej jen tam, kde potřebuješ ovládání nebo vlastní značky.

App\Core\Integrations\Maps\GoogleMapsLinks skládá adresy Googlu — čistá funkce bez stavu:

Metoda Vrací
embedUrl($apiKey, $lat, $lng, $address, $language, $zoom, $mapType) src pro <iframe>, nebo null bez klíče či místa
openUrl($lat, $lng, $address) odkaz „Otevřít v Google Mapách“; klíč nepotřebuje

Souřadnice mají přednost před adresou (jsou přesné); platné jsou jen v rozsahu ±90 / ±180. zoom se ořízne na 0–21, mapType je roadmap nebo satellite, jazyk se vezme jen ve tvaru cs nebo cs-cz. Parametry se skládají přes http_build_query (RFC 3986), takže se text adresy nikdy nevkládá do URL ručně.

Bezpečné vkládání textu do bubliny#

Text v bublině dynamické mapy (název inzerátu — zadává ho uživatel) se do stránky nikdy nevkládá jako HTML. googlemap.js ho čte z atributu data-info, vloží do uzlu <strong> přes textContent a ten předá InfoWindow jako DOM uzel — < a > se proto zobrazí doslova. Dřív se předával řetězec HTML, takže se text vložený do názvu inzerátu mohl v bublině vyhodnotit jako HTML. Hlídá to test tests/Unit/JavaScript/googlemap-no-html-info.test.mjs.

Skript je v každé šabloně a bundle se musí přebuildovat

googlemap.js je v assets/js/components/ každé šablony a vstupuje do jejího dist/bundle.js. Oprava skriptu proto znamená úpravu ve všech šablonách a přebuildování jejich bundlů (commit s opravou bubliny sáhl na všech 18). Starý bundle by opravu nenesl.

Další vlastnosti dynamické mapy: Google Maps se načítají s loading=async a knihovnami přes importLibrary; s vyplněným mapId se používá AdvancedMarkerElement (událost gmp-click), bez něj starší google.maps.Marker. S mapId Google ignoruje volbu styles — styl mapy se řídí v Cloud konzoli.

Dohledání adres po IPv4#

GoogleGeocoder::geocode() bere geocodingKey() a volá Google přes LocationHelper::lookupAddress() — ohraničený HTTPS GET (timeouty, bez přesměrování, strop velikosti odpovědi). Selhání se neloguje s URL, protože ta nese klíč. Požadavek jde po IPv4 (CURLOPT_IPRESOLVE = CURL_IPRESOLVE_V4, u záložní větve přes proud bindto => '0:0'): klíč pro server bývá omezený na IP adresu a IPv6 adresa stroje se mění, takže by neodpovídala povoleným. Bez klíče vrátí geokodér chybu a volající se vrátí ke středu obce.

reCAPTCHA#

App\Core\Forms\ReCaptcha\ReCaptchaRuntime rozhoduje za běhu, zda se na formulářích použije reCAPTCHA, která verze a s jakými klíči. Pořadí platnosti:

  1. security.reCaptcha: false v konfiguraci je tvrdé vypnutí (localhost, kde Google nefunguje; zapnuté připojení v databázi ji nesmí zapnout).
  2. Zapnuté připojení recaptcha_v3 s vlastním site key → v3.
  3. Jinak zapnuté recaptcha_v2.

Klíče žijí výhradně v připojení. Z konfigurace zmizely reCaptchaSiteKey i reCaptchaSecretKey, sekce recaptcha: a DI rozšíření Contributte; metodu addInvisibleReCaptcha() registruje jen ReCaptchaToggleExtension. Bez obou klíčů je reCAPTCHA vypnutá. v3 záložní klíče nemá.

Část Úkol
RuntimeReCaptchaBinding volí skutečné nebo vypnuté pole při stavbě formuláře, ne při kompilaci kontejneru; vypnutá varianta je DisabledReCaptchaField
RuntimeReCaptchaProvider::validate() nastaví práh skóre podle runtime (getMinimalScore(): v3 výchozí 0,5, v2 žádný práh)
pole formuláře nese data-version (v2 / v3), podle něj se zvolí tok na frontu
invisibleRecaptcha.js v2 vykreslí neviditelný widget; v3 načte api.js?render=<sitekey>, při odeslání execute() a token vloží do g-recaptcha-response; při chybě odešle bez tokenu jako v2

addInvisibleReCaptcha('recaptcha', true, $t('recaptcha.required')) je povinné ve všech 11 formulářích; prázdný token dostane tutéž hlášku jako odmítnutý. Dvě poznámky:

Nové pole reCAPTCHA = required: true

S false projde odeslání bez tokenu úplně. Hlášku recaptcha.required doplň do překladů formuláře (modulu), jinak se vypíše holý klíč.

JavaScript je kanonický v template2

invisibleRecaptcha.js se udržuje v template2 a do ostatních šablon se kopíruje skriptem php bin/theme-js-sync.php --apply. Po změně zvedni verzi v odkazu (?v=).

Postup: přidat nový typ připojení#

  1. Třída App\Core\Integrations\Connections\Types\<Nazev>Connection s ConnectionTypeInterface: konstanta CODE, getGroup() (null, nebo kód skupiny), getFields(). Pole, která smí do stránky, mají public: true; tajná Secret. Povinná pole mají required: true (kontrolují se jen u zapnutého připojení).
  2. Registrace v config/Shared/services.neon: třída do seznamu služeb a do pole registru externalConnectionTypes.
  3. Instalační SQL sql/<datum>_<NN>_install_<kód>_connection.sql:
INSERT IGNORE INTO `cms_system_external_connections` (`type`, `enabled`, `config`)
VALUES ('muj_typ', 0, NULL);

Idempotentní, vypnuté. Rollback s vysokým prefixem. 4. Překlady admin.system.presenters.externalConnection.form.types.<kód>.{title,description} a ….fields.<pole>.{label,description} v cs, en a sk (Locale/system/*/admin.*.neon). Chybějící klíč shodí kartu. 5. Spotřebitel: čtení přes ExternalConnections::isEnabled() / value(); vlastní třída, nebo rozšíření GoogleIntegrations. Čti jen u zapnutého a vyplněného připojení. Nikdy nevkládej do stránky tajné pole. 6. Test: normalizace polí, vypnuté připojení = null, chybějící řádek = null. 7. Nasazení: SQL před kódem (kód počítá s tabulkou, ale bez řádku se bere jako nenainstalované); po nasazení smaž temp/cache (nová služba v DI).

Admin stránka nic nepřidává ani nemaže

Karta se objeví jen u nainstalovaného řádku. Řádek, jehož typ kód nezná, se vypíše jako nepodporovaný a stránku nerozbije; upravit ho nejde.

Administrace#

Věc Detail
presenter Admin\System\Presenters\ExternalConnectionPresenter, adresa /admin/system-napojeni-tretich-stran/prehled, záložka sekce Nastavení
formulář jeden na řádek (connectionForm-<id> Multiplier); ExternalConnectionFormTrait
tajné pole addPassword, hodnota se do HTML nikdy nevypíše; prázdné odeslání = ponechat, vyplněné = přepsat, zaškrtnuté clear_<pole> = smazat
validace povinná pole jen u zapnutého; u tajného se za vyplněné počítá i uložená hodnota (bez zaškrtnutého „Smazat")
skupina zapnutím se vypnou ostatní zapnutá připojení téže skupiny (flash groupDisabled)
uložení jedna Api operace saveExclusive() v jedné DB transakci: zamkne řádky skupiny, zapíše kartu a vypne alternativy; při chybě se nezmění nic (ExternalConnectionGroupSaveTrait)
názvy polí předpona field_ a clear_, aby se pole typu nesrazilo s enabled ani jiným prvkem formuláře
po uložení karta zůstane otevřená (jednorázově přes session), ostatní sbalené
oprávnění admin :Admin:System:ExternalConnection:show; Api :Api:System:ExternalConnection:* jen administrátor

Api zdroj external-connection nese tajné klíče

config se z něj čte celý. RBAC (SQL …_02) povoluje čtení i zápis jen roli admin. Žádný grant pro guest ani registrated.

Ladicí panel (Tracy) ukazuje uložený tajný klíč v panelu SQL a dumpů

Na serveru s ostrými klíči debug nezapínej.

Migrace#

Pořadí při nasazení:

Soubor Co dělá
2026-10-02_01_system_external_connections.sql tabulka a řádek recaptcha_v2 (zapnuto, bez klíčů)
…_02_api_system_external_connection_privileges.sql Api RBAC (jen admin)
…_03_admin_external_connection_page.sql admin stránka a klíč :Admin:System:ExternalConnection:show
…_04_install_recaptcha_v3_connection.sql řádek recaptcha_v3 (vypnuto)
…_05_install_google_connections.sql čtyři Google typy; převezme hodnoty z cms_system_settings (vyplněná = zapnuto) — před nasazením kódu
…_06_remove_google_settings.sql smaže staré řádky nastavení — po nasazení kódu

Rollbacky …_91 až …_96. Po aplikaci _01–_03 smaž temp/cache a vyprázdni Redis (RBAC a router se cachují).

Kam sáhnout#

Chci Kde
typy připojení app/Core/Integrations/Connections/Types/
běhové čtení app/Core/Integrations/Connections/ExternalConnections.php
kategorie souhlasu s cookies config/Shared/cookieConsent.neon, app/Core/Cookie/CookieConsentConfig.php
reCAPTCHA app/Core/Forms/ReCaptcha/, app/Core/Forms/DI/ReCaptchaToggleExtension.php
administraci app/UI/Admin/System/Presenters/ExternalConnectionPresenter.php + Traits/Forms/ExternalConnectionFormTrait.php
registraci typů config/Shared/services.neon (externalConnectionTypes)

Nastavení cookies