diff --git a/configs/core.ini b/configs/core.ini index 74f641bd..a09a5feb 100644 --- a/configs/core.ini +++ b/configs/core.ini @@ -31,6 +31,12 @@ tiger.skin = "puma" ; default skin (Bootswatch Zephyr palette); swap live vi tiger.site.name = "Tiger" tiger.site.home_page = tiger.site.description = ; the site's one-line description — the og:description fallback +; "Built with Tiger" — the the public layout emits. SHIPPED +; ON: the technology surveys (W3Techs/BuiltWith/Wappalyzer) build their market-share tables off this +; tag, and an install that doesn't emit it is counted as nobody. NAME ONLY — a version here would +; hand a vulnerability scanner the exact build to match CVEs against. A site opts out in the admin +; (Settings > CMS, "Hide Tiger meta tag"), which writes the `config` row that overrides this line. +tiger.site.generator_meta = 1 ; --- SEO / social cards (TigerSEO) ----------------------------------------------------------- ; The share-card (Open Graph) tiers, resolved by Seo_Service_Head, each filling only what the diff --git a/library/Tiger/Application/Bootstrap.php b/library/Tiger/Application/Bootstrap.php index 503c7ef0..6681f387 100644 --- a/library/Tiger/Application/Bootstrap.php +++ b/library/Tiger/Application/Bootstrap.php @@ -522,6 +522,14 @@ protected function _initTheme() $site = ($cfg && $cfg->get('tiger')) ? $cfg->tiger->get('site') : null; $view->siteName = ($site && (string) $site->get('name') !== '') ? (string) $site->name : 'Tiger'; + // The public layout's generator meta: NAME ONLY, never a version. A version string would hand a + // vulnerability scanner the exact build to match CVEs against, across an install base nobody + // patches on our schedule. The shipped default is ON and lives in configs/core.ini + // (tiger.site.generator_meta); the admin's "Hide Tiger meta tag" writes the config row that + // overrides it. Absent entirely (a stripped ini) still reads as on — the flag fails VISIBLE. + $gen = $site ? (string) $site->get('generator_meta') : ''; + $view->showGenerator = ($gen === '' || filter_var($gen, FILTER_VALIDATE_BOOLEAN)); + Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer')->setView($view); // Global layout from the CHROME theme (the active theme for a 'site' theme; the base theme for a diff --git a/modules/cms/controllers/SettingsController.php b/modules/cms/controllers/SettingsController.php index 0d83ef5a..b734046f 100644 --- a/modules/cms/controllers/SettingsController.php +++ b/modules/cms/controllers/SettingsController.php @@ -36,6 +36,9 @@ public function indexAction() $form->populate([ 'site_name' => ($site && (string) $site->get('name') !== '') ? (string) $site->name : 'Tiger', 'home_page' => $home, + // Config holds the flag positively; the checkbox asks "hide?" — invert on the way in. + 'hide_generator' => ($site && (string) $site->get('generator_meta') !== '' + && !filter_var((string) $site->get('generator_meta'), FILTER_VALIDATE_BOOLEAN)) ? 1 : 0, ]); $this->view->title = 'Settings — Tiger Admin'; diff --git a/modules/cms/forms/Settings.php b/modules/cms/forms/Settings.php index 47119240..90bace7b 100644 --- a/modules/cms/forms/Settings.php +++ b/modules/cms/forms/Settings.php @@ -35,6 +35,12 @@ protected function elements(): array 'validators' => [['Regex', false, ['pattern' => self::HOME_PATTERN]]], 'attribs' => ['id' => 'set-home-page'], ]], + // The opt-out for the public . CHECKED = HIDDEN. + // The stored flag is the positive `tiger.site.generator_meta` (shipped on in core.ini); + // Cms_Service_Settings is the single place the two polarities are reconciled. + ['checkbox', 'hide_generator', [ + 'attribs' => ['id' => 'set-hide-generator', 'class' => 'form-check-input'], + ]], ]; } } diff --git a/modules/cms/languages/de/cms.php b/modules/cms/languages/de/cms.php index 8fe10822..92033d14 100644 --- a/modules/cms/languages/de/cms.php +++ b/modules/cms/languages/de/cms.php @@ -78,6 +78,8 @@ 'cms.settings.help_site_name' => 'Wird im Website-Header und im Browser-Tab angezeigt und als Standard-Seitentitel verwendet.', 'cms.settings.label_home_page' => 'Startseite', 'cms.settings.help_home_page' => 'Welche Seite unter %s ausgeliefert wird. „Integrierte Landingpage“ behält die Standard-Marketingseite bei.', + 'cms.settings.label_hide_generator' => 'Tiger-Meta-Tag ausblenden', + 'cms.settings.help_hide_generator' => 'Öffentliche Seiten enthalten ein kleines „generator“-Tag, das Tiger nennt. Eine Versionsnummer steht nie darin. Aktivieren Sie dies, um es wegzulassen.', 'cms.settings.opt_builtin_landing' => '— Integrierte Landingpage —', 'cms.settings.optgroup_general' => 'Allgemein', 'cms.settings.optgroup_pages' => 'Inhaltsseiten', diff --git a/modules/cms/languages/en/cms.php b/modules/cms/languages/en/cms.php index cc6df69c..60856ab3 100644 --- a/modules/cms/languages/en/cms.php +++ b/modules/cms/languages/en/cms.php @@ -80,6 +80,8 @@ 'cms.settings.help_site_name' => 'Shown in the site header and browser tab, and used as the default page title.', 'cms.settings.label_home_page' => 'Home page', 'cms.settings.help_home_page' => 'Which page to serve at %s. “Built-in landing page” keeps the default marketing page.', + 'cms.settings.label_hide_generator' => 'Hide Tiger meta tag', + 'cms.settings.help_hide_generator' => 'Public pages carry a small “generator” tag naming Tiger. It never includes a version number. Tick this to leave it out.', 'cms.settings.opt_builtin_landing' => '— Built-in landing page —', 'cms.settings.optgroup_general' => 'General', 'cms.settings.optgroup_pages' => 'Content pages', diff --git a/modules/cms/languages/es/cms.php b/modules/cms/languages/es/cms.php index a277d136..7819e435 100644 --- a/modules/cms/languages/es/cms.php +++ b/modules/cms/languages/es/cms.php @@ -78,6 +78,8 @@ 'cms.settings.help_site_name' => 'Se muestra en el encabezado del sitio y la pestaña del navegador, y se usa como título de página predeterminado.', 'cms.settings.label_home_page' => 'Página de inicio', 'cms.settings.help_home_page' => 'Qué página servir en %s. «Página de inicio integrada» mantiene la página de marketing predeterminada.', + 'cms.settings.label_hide_generator' => 'Ocultar la metaetiqueta de Tiger', + 'cms.settings.help_hide_generator' => 'Las páginas públicas incluyen una pequeña etiqueta «generator» que nombra a Tiger. Nunca incluye un número de versión. Marca esta casilla para omitirla.', 'cms.settings.opt_builtin_landing' => '— Página de inicio integrada —', 'cms.settings.optgroup_general' => 'General', 'cms.settings.optgroup_pages' => 'Páginas de contenido', diff --git a/modules/cms/languages/fr/cms.php b/modules/cms/languages/fr/cms.php index 6fe6fb67..4091c5f1 100644 --- a/modules/cms/languages/fr/cms.php +++ b/modules/cms/languages/fr/cms.php @@ -78,6 +78,8 @@ 'cms.settings.help_site_name' => 'Affiché dans l\'en-tête du site et l\'onglet du navigateur, et utilisé comme titre de page par défaut.', 'cms.settings.label_home_page' => 'Page d\'accueil', 'cms.settings.help_home_page' => 'Quelle page servir sur %s. « Page d\'accueil intégrée » conserve la page marketing par défaut.', + 'cms.settings.label_hide_generator' => 'Masquer la balise meta Tiger', + 'cms.settings.help_hide_generator' => 'Les pages publiques contiennent une petite balise « generator » qui nomme Tiger. Elle ne contient jamais de numéro de version. Cochez cette case pour l\'omettre.', 'cms.settings.opt_builtin_landing' => '— Page d\'accueil intégrée —', 'cms.settings.optgroup_general' => 'Général', 'cms.settings.optgroup_pages' => 'Pages de contenu', diff --git a/modules/cms/languages/hi/cms.php b/modules/cms/languages/hi/cms.php index bf9cd728..b1274ffb 100644 --- a/modules/cms/languages/hi/cms.php +++ b/modules/cms/languages/hi/cms.php @@ -78,6 +78,8 @@ 'cms.settings.help_site_name' => 'साइट हेडर और ब्राउज़र टैब में दिखाया जाता है, और डिफ़ॉल्ट पेज शीर्षक के रूप में उपयोग किया जाता है।', 'cms.settings.label_home_page' => 'होम पेज', 'cms.settings.help_home_page' => '%s पर कौन सा पेज परोसना है। “बिल्ट-इन लैंडिंग पेज” डिफ़ॉल्ट मार्केटिंग पेज बनाए रखता है।', + 'cms.settings.label_hide_generator' => 'Tiger मेटा टैग छिपाएँ', + 'cms.settings.help_hide_generator' => 'सार्वजनिक पृष्ठों में एक छोटा “generator” टैग होता है जो Tiger का नाम देता है। इसमें कभी भी संस्करण संख्या नहीं होती। इसे छोड़ने के लिए यह चुनें।', 'cms.settings.opt_builtin_landing' => '— बिल्ट-इन लैंडिंग पेज —', 'cms.settings.optgroup_general' => 'सामान्य', 'cms.settings.optgroup_pages' => 'सामग्री पृष्ठ', diff --git a/modules/cms/languages/pt/cms.php b/modules/cms/languages/pt/cms.php index b8d4e81f..a1a112e7 100644 --- a/modules/cms/languages/pt/cms.php +++ b/modules/cms/languages/pt/cms.php @@ -78,6 +78,8 @@ 'cms.settings.help_site_name' => 'Exibido no cabeçalho do site e na aba do navegador, e usado como título de página padrão.', 'cms.settings.label_home_page' => 'Página inicial', 'cms.settings.help_home_page' => 'Qual página servir em %s. “Página inicial integrada” mantém a página de marketing padrão.', + 'cms.settings.label_hide_generator' => 'Ocultar a meta tag do Tiger', + 'cms.settings.help_hide_generator' => 'As páginas públicas incluem uma pequena tag “generator” que menciona o Tiger. Nunca inclui um número de versão. Marque esta opção para omiti-la.', 'cms.settings.opt_builtin_landing' => '— Página inicial integrada —', 'cms.settings.optgroup_general' => 'Geral', 'cms.settings.optgroup_pages' => 'Páginas de conteúdo', diff --git a/modules/cms/languages/tlh/cms.php b/modules/cms/languages/tlh/cms.php index 72644470..b056df2d 100644 --- a/modules/cms/languages/tlh/cms.php +++ b/modules/cms/languages/tlh/cms.php @@ -81,6 +81,8 @@ 'cms.settings.help_site_name' => 'Shown in the site header and browser tab, and used as the default page title.', 'cms.settings.label_home_page' => 'Home page', 'cms.settings.help_home_page' => 'Which page to serve at %s. “Built-in landing page” keeps the default marketing page.', + 'cms.settings.label_hide_generator' => 'Hide Tiger meta tag', + 'cms.settings.help_hide_generator' => 'Public pages carry a small “generator” tag naming Tiger. It never includes a version number. Tick this to leave it out.', 'cms.settings.opt_builtin_landing' => '— Built-in landing page —', // ---- Content list ---- diff --git a/modules/cms/services/Settings.php b/modules/cms/services/Settings.php index a06e685e..55a00a28 100644 --- a/modules/cms/services/Settings.php +++ b/modules/cms/services/Settings.php @@ -38,6 +38,12 @@ public function save(array $params): void // free-typed path) put in the field. IndexController resolves it; the form validated its shape. $cfg->set($g, '', 'tiger.site.home_page', trim((string) $v['home_page'])); + // The flag is stored POSITIVELY (tiger.site.generator_meta, shipped = 1 in configs/core.ini); + // the checkbox asks the opposite question, so this is the ONE place the two are reconciled. + // An unchecked box doesn't submit at all, so cms.settings.js sends an explicit "0" — + // otherwise un-ticking "hide" could never turn the tag back on. + $cfg->set($g, '', 'tiger.site.generator_meta', !empty($v['hide_generator']) ? '0' : '1'); + $this->_success([], 'cms.settings.saved', '/cms/settings'); } catch (Throwable $e) { $this->_error(APPLICATION_ENV !== 'production' ? $e->getMessage() : 'core.api.error.general'); diff --git a/modules/cms/views/scripts/settings/index.phtml b/modules/cms/views/scripts/settings/index.phtml index 983d7c8d..ae594165 100644 --- a/modules/cms/views/scripts/settings/index.phtml +++ b/modules/cms/views/scripts/settings/index.phtml @@ -10,6 +10,7 @@ */ $form = $this->form; $el = function ($name) use ($form) { return $form->getElement($name); }; +$hideGenerator = !empty($el('hide_generator')->getValue()); ?>
@@ -35,7 +36,7 @@ $el = function ($name) use ($form) { return $form->getElement($name); };
t('cms.settings.help_site_name') ?>
-
+
@@ -61,6 +62,12 @@ $el = function ($name) use ($form) { return $form->getElement($name); };
t('cms.settings.help_home_page', '/') ?>
+
+
+ > + +
t('cms.settings.help_hide_generator') ?>
+
diff --git a/tests/Integration/Cms/SettingsServiceTest.php b/tests/Integration/Cms/SettingsServiceTest.php index 9dbe21d9..b1266a3d 100644 --- a/tests/Integration/Cms/SettingsServiceTest.php +++ b/tests/Integration/Cms/SettingsServiceTest.php @@ -104,6 +104,52 @@ public function save_trims_the_site_name(): void $this->assertSame('Spaced Site', $this->globalConfig('tiger.site.name')); } + // ----- the generator-meta flag -------------------------------------------------------------- + + #[Test] + public function the_flag_is_stored_positively_and_defaults_to_on(): void + { + $this->loginAs('admin'); + // The box unticked doesn't submit at all; cms.settings.js sends the explicit '0' modelled here. + $this->call('save', ['site_name' => 'S', 'home_page' => '', 'hide_generator' => '0']); + + $this->assertSame('1', $this->globalConfig('tiger.site.generator_meta'), + 'not hidden is stored as generator_meta=1 — the same polarity configs/core.ini ships'); + } + + #[Test] + public function ticking_hide_turns_the_flag_off(): void + { + $this->loginAs('admin'); + $res = $this->call('save', ['site_name' => 'S', 'home_page' => '', 'hide_generator' => '1']); + + $this->assertSame(1, (int) $res->result); + $this->assertSame('0', $this->globalConfig('tiger.site.generator_meta'), + 'the checkbox asks the opposite question from the flag; the service reconciles it'); + } + + #[Test] + public function un_ticking_hide_turns_the_flag_back_on(): void + { + // The regression this guards: an unchecked checkbox submits NOTHING, so without the explicit + // '0' from the JS the flag would keep its old value and the tag could never come back. + $this->loginAs('admin'); + $this->call('save', ['site_name' => 'S', 'home_page' => '', 'hide_generator' => '1']); + $this->call('save', ['site_name' => 'S', 'home_page' => '', 'hide_generator' => '0']); + + $this->assertSame('1', $this->globalConfig('tiger.site.generator_meta'), 'the opt-out is reversible'); + } + + #[Test] + public function a_missing_param_leaves_the_tag_on(): void + { + $this->loginAs('admin'); + $this->call('save', ['site_name' => 'S', 'home_page' => '']); + + $this->assertSame('1', $this->globalConfig('tiger.site.generator_meta'), + 'absent reads as not-hidden — nothing silently opts a site out'); + } + // ----- validation --------------------------------------------------------------------------- #[Test] diff --git a/tests/Unit/View/GeneratorMetaTest.php b/tests/Unit/View/GeneratorMetaTest.php new file mode 100644 index 00000000..2a8ca119 --- /dev/null +++ b/tests/Unit/View/GeneratorMetaTest.php @@ -0,0 +1,121 @@ +`. + * + * Two things are worth proving, and neither is provable by reading the file: + * + * 1. The GATE works. The tag is on by default so every install is countable by the technology + * surveys (W3Techs/BuiltWith/Wappalyzer) — that visibility is the whole point of shipping it — + * and a site hides it with "Hide Tiger meta tag" (Settings > CMS > Site Settings), which sets + * `tiger.site.generator_meta` to `0` and makes Tiger_Application_Bootstrap seed `showGenerator = false`. + * A view built outside the bootstrap has no such property at all, and must still emit. + * So the real block is lifted out of the shipped layout and RENDERED in all three states. + * + * 2. It carries NAME ONLY. A version string here would hand a vulnerability scanner the exact build + * to match CVEs against, across an install base nobody patches on our schedule. The content must + * stay a literal — no `tmp !== '' && is_dir($this->tmp)) { + foreach ((array) glob($this->tmp . '/*') as $f) { @unlink($f); } + @rmdir($this->tmp); + } + parent::tearDown(); + } + + /** The generator block as the layout actually ships it. */ + private function block(): string + { + $layout = (string) file_get_contents(self::LAYOUT); + $ok = preg_match('~^<\?php if \(.*showGenerator.*\n.*\n<\?php endif; \?>$~m', $layout, $m); + $this->assertSame(1, $ok, 'the layout still carries a showGenerator-gated block'); + return $m[0]; + } + + /** Render that block with $this->showGenerator in a given state ('unset' = never assigned). */ + private function render($state): string + { + $this->tmp = $this->tmp ?: (string) tempnam(sys_get_temp_dir(), 'gen'); + if (is_file($this->tmp)) { unlink($this->tmp); mkdir($this->tmp, 0700); } + file_put_contents($this->tmp . '/frag.phtml', $this->block()); + + $view = new Zend_View(); + $view->addScriptPath($this->tmp); + if ($state !== 'unset') { $view->showGenerator = $state; } + + ob_start(); // beStrictAboutOutputDuringTests + $out = $view->render('frag.phtml'); + ob_end_clean(); + return (string) $out; + } + + // ----- the gate ----------------------------------------------------------------------------- + + #[Test] + public function it_emits_the_tag_when_the_site_has_not_hidden_it(): void + { + $this->assertStringContainsString('', $this->render(true)); + } + + #[Test] + public function it_emits_nothing_when_the_site_hides_it(): void + { + $this->assertSame('', trim($this->render(false)), 'hidden means no tag at all, not an empty one'); + } + + #[Test] + public function it_defaults_to_emitting_when_the_view_was_built_outside_the_bootstrap(): void + { + $this->assertStringContainsString('', $this->render('unset')); + } + + // ----- the shipped default ------------------------------------------------------------------ + + #[Test] + public function core_ini_ships_the_flag_on_so_the_default_is_declared_not_inferred(): void + { + // A feature flag needs a stated default. Leaving it to the absence of a key means nobody can + // read the shipped behaviour off the config, and "on" would be an accident of the code. + $ini = parse_ini_file(TIGER_CORE_PATH . '/configs/core.ini', true, INI_SCANNER_RAW); + + $this->assertArrayHasKey('tiger.site.generator_meta', $ini['production'], + 'the default lives in the stock core ini, beside the other site-identity keys'); + $this->assertSame('1', (string) $ini['production']['tiger.site.generator_meta'], + 'shipped ON — an install that emits nothing is counted by nobody'); + } + + // ----- name only, forever ------------------------------------------------------------------- + + #[Test] + public function the_content_is_a_literal_and_never_carries_a_version(): void + { + $block = $this->block(); + $this->assertMatchesRegularExpression( + '~~', $block, + 'content is the bare product name' + ); + $this->assertDoesNotMatchRegularExpression( + '~content="Tiger[^"]~', $block, + 'nothing may follow "Tiger" inside content — a version here is a gift to vulnerability scanners' + ); + foreach (['version'] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $block, "no $forbidden in the generator meta"); + } + } +} diff --git a/themes/puma/assets/js/cms.settings.js b/themes/puma/assets/js/cms.settings.js index cc76a682..c4b5631e 100644 --- a/themes/puma/assets/js/cms.settings.js +++ b/themes/puma/assets/js/cms.settings.js @@ -24,6 +24,8 @@ if (window.TigerPathbox) { TigerPathbox.flushAll(form); } var fd = new URLSearchParams(new FormData(form)); + // An unchecked box doesn't submit — send an explicit "0" so un-ticking it persists. + if (!fd.has('hide_generator')) { fd.set('hide_generator', '0'); } fd.set('module', 'cms'); fd.set('service', 'settings'); fd.set('method', 'save'); TigerButton.run(btn, function () { diff --git a/themes/puma/layouts/scripts/layout.phtml b/themes/puma/layouts/scripts/layout.phtml index 5e0d552b..c3fcb667 100644 --- a/themes/puma/layouts/scripts/layout.phtml +++ b/themes/puma/layouts/scripts/layout.phtml @@ -34,6 +34,12 @@ $_lang = defined('LANG') ? LANG : 'en'; ?> headTitle() ?> headMeta() ?> + +showGenerator) || $this->showGenerator): ?> + +