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í. publicu 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 Googlesiteverifynerozliší špatný tajný klíč bez skutečného tokenu z prohlížeče (vrací vždyinvalid-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
- Přidej
{key: <klíč>, readonly: false}docategories. - Doplň překlady
front.base.components.cookieBar.categories.<klíč>.titlea.descriptionve všech jazycích (app/Locale/base/<locale>/front.<LOCALE>.neon). - Má-li se kategorie mapovat na Google Consent Mode nebo při odvolání mazat cookies,
doplň
CONSENT_MODE_SIGNALSaCATEGORY_COOKIESvwww/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. - 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.
GoogleMapsLinks#
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:
security.reCaptcha: falsev konfiguraci je tvrdé vypnutí (localhost, kde Google nefunguje; zapnuté připojení v databázi ji nesmí zapnout).- Zapnuté připojení
recaptcha_v3s vlastním site key → v3. - 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í#
- Třída
App\Core\Integrations\Connections\Types\<Nazev>ConnectionsConnectionTypeInterface: konstantaCODE,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í). - Registrace v
config/Shared/services.neon: třída do seznamu služeb a do pole registruexternalConnectionTypes. - 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) |