Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions configs/core.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 <meta name="generator" content="Tiger"> 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
Expand Down
8 changes: 8 additions & 0 deletions library/Tiger/Application/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions modules/cms/controllers/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 6 additions & 0 deletions modules/cms/forms/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <meta name="generator" content="Tiger">. 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'],
]],
];
}
}
2 changes: 2 additions & 0 deletions modules/cms/languages/de/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions modules/cms/languages/en/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions modules/cms/languages/es/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions modules/cms/languages/fr/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions modules/cms/languages/hi/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => 'सामग्री पृष्ठ',
Expand Down
2 changes: 2 additions & 0 deletions modules/cms/languages/pt/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions modules/cms/languages/tlh/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----
Expand Down
6 changes: 6 additions & 0 deletions modules/cms/services/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
9 changes: 8 additions & 1 deletion modules/cms/views/scripts/settings/index.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/
$form = $this->form;
$el = function ($name) use ($form) { return $form->getElement($name); };
$hideGenerator = !empty($el('hide_generator')->getValue());
?>
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
Expand All @@ -35,7 +36,7 @@ $el = function ($name) use ($form) { return $form->getElement($name); };
<?= $el('site_name') ?>
<div class="form-text"><?= $this->t('cms.settings.help_site_name') ?></div>
</div>
<div class="mb-0">
<div class="mb-3">
<label class="form-label" for="set-home-page-search"><?= $this->t('cms.settings.label_home_page') ?></label>

<?php // The committed value the form submits — hidden; the combobox writes into it. ?>
Expand All @@ -61,6 +62,12 @@ $el = function ($name) use ($form) { return $form->getElement($name); };

<div class="form-text"><?= $this->t('cms.settings.help_home_page', '<code>/</code>') ?></div>
</div>
<hr class="my-3">
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" id="set-hide-generator" name="hide_generator" value="1" <?= $hideGenerator ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="set-hide-generator"><?= $this->t('cms.settings.label_hide_generator') ?></label>
<div class="form-text"><?= $this->t('cms.settings.help_hide_generator') ?></div>
</div>
</div>
</div>
</div>
Expand Down
46 changes: 46 additions & 0 deletions tests/Integration/Cms/SettingsServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
121 changes: 121 additions & 0 deletions tests/Unit/View/GeneratorMetaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.

namespace Tiger\Tests\Unit\View;

use PHPUnit\Framework\Attributes\Test;
use Tiger\Tests\Support\UnitTestCase;
use Zend_View;

/**
* The public layout's `<meta name="generator" content="Tiger">`.
*
* 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 `<?=`, no Tiger_Version, no interpolation of any kind.
*/
final class GeneratorMetaTest extends UnitTestCase
{
private const LAYOUT = TIGER_CORE_PATH . '/themes/puma/layouts/scripts/layout.phtml';

private string $tmp = '';

protected function tearDown(): void
{
if ($this->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('<meta name="generator" content="Tiger">', $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('<meta name="generator" content="Tiger">', $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(
'~<meta name="generator" content="Tiger">~', $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 (['<?=', 'Tiger_Version', 'VERSION', '$this->version'] as $forbidden) {
$this->assertStringNotContainsString($forbidden, $block, "no $forbidden in the generator meta");
}
}
}
Loading
Loading