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
77 changes: 71 additions & 6 deletions core/controllers/IndexController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,22 @@ public function init()
*/
public function indexAction()
{
// 1) An admin picked a CMS page as the home page (tiger.site.home_page)? Serve it.
$homeId = $this->_homePageId();
if ($homeId !== '') {
$page = (new Tiger_Model_Page())->findById($homeId);
$home = $this->_homePageId();

// 1a) An admin picked a PATH — a public module page (`/marketplace`, `/docs`) or any ad-hoc
// route. Forwarded, not redirected: the content has to be served AT "/" or it isn't the
// home page, it's a signpost pointing away from it.
if ($home !== '' && $home[0] === '/') {
if ($this->_forwardToPath($home)) { return; }
// Unresolvable (the module was deactivated, the route retired) — fall through to the
// theme home / built-in landing rather than 404ing the site's front door.
}

// 1b) An admin picked a CMS page as the home page (tiger.site.home_page)? Serve it.
if ($home !== '' && $home[0] !== '/') {
$page = (new Tiger_Model_Page())->findById($home);
if ($page && $page->status === Tiger_Model_Page::STATUS_PUBLISHED) {
$this->_forward('view', 'page', null, ['cms_page_id' => $homeId]);
$this->_forward('view', 'page', null, ['cms_page_id' => $home]);
return;
}
}
Expand Down Expand Up @@ -164,7 +174,62 @@ public function techStackAction()
$this->view->localeView();
}

/** The configured home-page id (tiger.site.home_page), or '' for the built-in landing. */
/**
* Forward "/" to an internal path — a public module page (`/marketplace`, `/docs`) or any
* `module/controller/action`.
*
* Resolution mirrors `Tiger_Controller_Plugin_RouteOverride` on purpose: a module's PUBLIC page
* is usually a registered override (a pretty prefix → a canonical MVC target), so matching the
* override table first is what makes `/marketplace` work rather than only the long canonical
* path. Anything not in that table falls back to a plain `module/controller/action` parse.
*
* Segments are sanitized to the same `[a-zA-Z0-9_-]` shape the dispatcher accepts, so a stored
* value can't be used to reach outside normal dispatch.
*
* @param string $path the configured path, leading slash included
* @return bool true when the request was forwarded
*/
protected function _forwardToPath($path)
{
$clean = trim(parse_url($path, PHP_URL_PATH) ?: '', '/');
if ($clean === '') { return false; }

// A registered module override (the pretty public prefix).
if (class_exists('Tiger_Routing_Overrides')) {
foreach (Tiger_Routing_Overrides::all() as $o) {
$prefix = (string) $o['prefix'];
if ($clean !== $prefix && strpos($clean, $prefix . '/') !== 0) { continue; }
[$module, $controller, $action] = $o['mca'];
$slug = trim(substr($clean, strlen($prefix)), '/');
$this->_forward($action, $controller, $module, $slug !== '' ? ['slug' => $slug] : []);
return true;
}
}

// Else a canonical module/controller/action path.
$seg = array_values(array_filter(explode('/', $clean), 'strlen'));
$ok = static function ($s) { return (string) preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $s); };
$module = $ok($seg[0] ?? '');
$controller = $ok($seg[1] ?? 'index');
$action = $ok($seg[2] ?? 'index');
if ($module === '') { return false; }

$front = Zend_Controller_Front::getInstance();
$dirs = (array) $front->getControllerDirectory();
if (!isset($dirs[$module])) {
// Not a module — treat the first segment as a default-namespace controller (/vibe).
$this->_forward($ok($seg[1] ?? 'index'), $module, null);
return true;
}

$this->_forward($action, $controller, $module);
return true;
}

/**
* The configured home page (`tiger.site.home_page`): a CMS `page_id`, a PATH beginning with "/",
* or '' for the built-in landing.
*/
protected function _homePageId()
{
if (!Zend_Registry::isRegistered('Zend_Config')) {
Expand Down
16 changes: 14 additions & 2 deletions modules/cms/controllers/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,22 @@ public function indexAction()
$tiger = $cfg->get('tiger');
$site = $tiger ? $tiger->get('site') : null;

$home = $site ? (string) $site->get('home_page') : '';

// A stored PATH that isn't one of the offered module pages (an ad-hoc route, or a module
// since deactivated) must still round-trip: show it in the custom field with "custom path"
// selected, rather than silently resetting the site's home page to the built-in landing.
$custom = '';
if ($home !== '' && $home[0] === '/' && !array_key_exists($home, Cms_Form_Settings::modulePaths())) {
$custom = $home;
$home = Cms_Form_Settings::CUSTOM;
}

$form = new Cms_Form_Settings();
$form->populate([
'site_name' => ($site && (string) $site->get('name') !== '') ? (string) $site->name : 'Tiger',
'home_page' => $site ? (string) $site->get('home_page') : '',
'site_name' => ($site && (string) $site->get('name') !== '') ? (string) $site->name : 'Tiger',
'home_page' => $home,
'home_page_custom' => $custom,
]);

$this->view->title = 'Settings — Tiger Admin';
Expand Down
50 changes: 48 additions & 2 deletions modules/cms/forms/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,61 @@
*/
class Cms_Form_Settings extends Tiger_Form
{
/** Sentinel option value meaning "use the typed path instead". Never stored. */
const CUSTOM = '__custom__';

/**
* Public module pages, as `path => label` — the active modules' pretty public prefixes.
*
* Read from `Tiger_Routing_Overrides`, which is where a module declares the public alias it wants
* (`/docs`, `/marketplace`). Non-page endpoints are skipped: an override whose prefix looks like a
* file (`robots.txt`, `sitemap.xml`, `llms.txt`) serves plain text, and offering it as a home page
* would only ever be a mistake. Reserved prefixes are already excluded by `all()`.
*
* @return array<string,string>
*/
public static function modulePaths(): array
{
if (!class_exists('Tiger_Routing_Overrides')) { return []; }

$out = [];
foreach (Tiger_Routing_Overrides::all() as $o) {
$prefix = trim((string) ($o['prefix'] ?? ''), '/');
if ($prefix === '' || strpos($prefix, '.') !== false) { continue; } // robots.txt / sitemap.xml / llms.txt
$out['/' . $prefix] = '/' . $prefix;
}
ksort($out);
return $out;
}

protected function elements(): array
{
$control = ['class' => 'form-control'];
$select = ['class' => 'form-select'];

$home = ['' => $this->_t('cms.settings.opt_builtin_landing')];
$pm = new Tiger_Model_Page();

// CMS pages — stored as a page_id.
$pages = [];
$pm = new Tiger_Model_Page();
foreach ($pm->fetchAll(
$pm->activeSelect()
->where('type = ?', Tiger_Model_Page::TYPE_PAGE)
->where('status = ?', Tiger_Model_Page::STATUS_PUBLISHED)
->order(['title ASC', 'locale ASC'])
) as $p) {
$label = ($p->title ?: $p->slug ?: $p->page_key) . ' (' . $p->locale . ')';
$home[$p->page_id] = $label;
$pages[$p->page_id] = $label;
}
if ($pages) { $home[$this->_t('cms.settings.optgroup_pages')] = $pages; }

// Public module pages — stored as a PATH. An active module's pretty public prefix is exactly
// what an admin thinks of as "the marketplace page" or "the docs page".
$modulePages = self::modulePaths();
if ($modulePages) { $home[$this->_t('cms.settings.optgroup_modules')] = $modulePages; }

// The escape hatch: any other route, typed in.
$home[self::CUSTOM] = $this->_t('cms.settings.opt_custom_path');

return [
['text', 'site_name', [
Expand All @@ -40,6 +79,13 @@ protected function elements(): array
'multiOptions' => $home,
'attribs' => array_merge($select, ['id' => 'set-home-page']),
]],
// Revealed by the view when "custom path" is picked; its value replaces home_page on save.
['text', 'home_page_custom', [
'required' => false,
'filters' => ['StringTrim'],
'validators' => [['Regex', false, ['pattern' => '~^/[A-Za-z0-9/_\-.]*$~']]],
'attribs' => array_merge($control, ['id' => 'set-home-page-custom', 'placeholder' => '/marketplace']),
]],
];
}
}
5 changes: 5 additions & 0 deletions modules/cms/languages/de/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
'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.opt_builtin_landing' => '— Integrierte Landingpage —',
'cms.settings.optgroup_pages' => 'Inhaltsseiten',
'cms.settings.optgroup_modules' => 'Modulseiten',
'cms.settings.opt_custom_path' => '— Eigener Pfad… —',
'cms.settings.label_home_page_custom' => 'Eigener Pfad',
'cms.settings.help_home_page_custom' => 'Eine beliebige Route dieser Website, beginnend mit einem Schrägstrich — zum Beispiel /marketplace. Sie wird unter / ausgeliefert, nicht per Weiterleitung.',

// ---- Content list ----
'cms.page.list_heading' => 'Inhalt',
Expand Down
5 changes: 5 additions & 0 deletions modules/cms/languages/en/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@
'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.opt_builtin_landing' => '— Built-in landing page —',
'cms.settings.optgroup_pages' => 'Content pages',
'cms.settings.optgroup_modules' => 'Module pages',
'cms.settings.opt_custom_path' => '— Custom path… —',
'cms.settings.label_home_page_custom' => 'Custom path',
'cms.settings.help_home_page_custom' => 'Any route on this site, starting with a slash — for example /marketplace. It is served at / rather than redirected to.',

// ---- Content list ----
'cms.page.list_heading' => 'Content',
Expand Down
5 changes: 5 additions & 0 deletions modules/cms/languages/es/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
'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.opt_builtin_landing' => '— Página de inicio integrada —',
'cms.settings.optgroup_pages' => 'Páginas de contenido',
'cms.settings.optgroup_modules' => 'Páginas de módulos',
'cms.settings.opt_custom_path' => '— Ruta personalizada… —',
'cms.settings.label_home_page_custom' => 'Ruta personalizada',
'cms.settings.help_home_page_custom' => 'Cualquier ruta de este sitio, empezando por una barra: por ejemplo /marketplace. Se sirve en / en lugar de redirigir.',

// ---- Content list ----
'cms.page.list_heading' => 'Contenido',
Expand Down
5 changes: 5 additions & 0 deletions modules/cms/languages/fr/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
'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.opt_builtin_landing' => '— Page d\'accueil intégrée —',
'cms.settings.optgroup_pages' => 'Pages de contenu',
'cms.settings.optgroup_modules' => 'Pages de modules',
'cms.settings.opt_custom_path' => '— Chemin personnalisé… —',
'cms.settings.label_home_page_custom' => 'Chemin personnalisé',
'cms.settings.help_home_page_custom' => 'N\'importe quelle route de ce site, commençant par une barre oblique — par exemple /marketplace. Elle est servie sur / et non redirigée.',

// ---- Content list ----
'cms.page.list_heading' => 'Contenu',
Expand Down
5 changes: 5 additions & 0 deletions modules/cms/languages/hi/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
'cms.settings.label_home_page' => 'होम पेज',
'cms.settings.help_home_page' => '%s पर कौन सा पेज परोसना है। “बिल्ट-इन लैंडिंग पेज” डिफ़ॉल्ट मार्केटिंग पेज बनाए रखता है।',
'cms.settings.opt_builtin_landing' => '— बिल्ट-इन लैंडिंग पेज —',
'cms.settings.optgroup_pages' => 'सामग्री पृष्ठ',
'cms.settings.optgroup_modules' => 'मॉड्यूल पृष्ठ',
'cms.settings.opt_custom_path' => '— कस्टम पथ… —',
'cms.settings.label_home_page_custom' => 'कस्टम पथ',
'cms.settings.help_home_page_custom' => 'इस साइट का कोई भी रूट, स्लैश से शुरू — उदाहरण के लिए /marketplace। इसे रीडायरेक्ट किए बिना / पर ही परोसा जाता है।',

// ---- Content list ----
'cms.page.list_heading' => 'सामग्री',
Expand Down
5 changes: 5 additions & 0 deletions modules/cms/languages/pt/cms.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
'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.opt_builtin_landing' => '— Página inicial integrada —',
'cms.settings.optgroup_pages' => 'Páginas de conteúdo',
'cms.settings.optgroup_modules' => 'Páginas de módulos',
'cms.settings.opt_custom_path' => '— Caminho personalizado… —',
'cms.settings.label_home_page_custom' => 'Caminho personalizado',
'cms.settings.help_home_page_custom' => 'Qualquer rota deste site, começando com uma barra — por exemplo /marketplace. É servida em / em vez de redirecionar.',

// ---- Content list ----
'cms.page.list_heading' => 'Conteúdo',
Expand Down
11 changes: 10 additions & 1 deletion modules/cms/services/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ public function save(array $params): void
$cfg = new Tiger_Model_Config();
$g = Tiger_Model_Config::SCOPE_GLOBAL;
$cfg->set($g, '', 'tiger.site.name', trim((string) $v['site_name']));
$cfg->set($g, '', 'tiger.site.home_page', (string) $v['home_page']);

// The stored value is a CMS page_id, a PATH ("/marketplace"), or '' for the built-in
// landing. Picking "custom path" swaps in the typed value; a blank one falls back to the
// built-in rather than storing the sentinel, which would resolve to nothing.
$home = (string) $v['home_page'];
if ($home === Cms_Form_Settings::CUSTOM) {
$custom = trim((string) ($v['home_page_custom'] ?? ''));
$home = ($custom !== '' && $custom[0] === '/') ? $custom : '';
}
$cfg->set($g, '', 'tiger.site.home_page', $home);

$this->_success([], 'cms.settings.saved', '/cms/settings');
} catch (Throwable $e) {
Expand Down
19 changes: 19 additions & 0 deletions modules/cms/views/scripts/settings/index.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ $el = function ($name) use ($form) { return $form->getElement($name); };
<label class="form-label" for="<?= $this->escape($el('home_page')->getId()) ?>"><?= $this->t('cms.settings.label_home_page') ?></label>
<?= $el('home_page') ?>
<div class="form-text"><?= $this->t('cms.settings.help_home_page', '<code>/</code>') ?></div>

<?php // Revealed only for "custom path"; its value replaces home_page on save. ?>
<div id="home-page-custom-wrap" class="mt-3<?= $el('home_page')->getValue() === Cms_Form_Settings::CUSTOM ? '' : ' d-none' ?>">
<label class="form-label" for="<?= $this->escape($el('home_page_custom')->getId()) ?>"><?= $this->t('cms.settings.label_home_page_custom') ?></label>
<?= $el('home_page_custom') ?>
<div class="form-text"><?= $this->t('cms.settings.help_home_page_custom') ?></div>
</div>
</div>
</div>
</div>
Expand All @@ -56,6 +63,18 @@ document.addEventListener('DOMContentLoaded', function () {
var form = document.getElementById('cms-settings-form');
var fb = document.getElementById('cms-settings-feedback');

// Home page: reveal the free-text path field only when "custom path" is chosen.
var homeSel = document.getElementById('set-home-page');
var homeWrap = document.getElementById('home-page-custom-wrap');
if (homeSel && homeWrap) {
var syncHome = function () {
var custom = homeSel.value === <?= json_encode(Cms_Form_Settings::CUSTOM) ?>;
homeWrap.classList.toggle('d-none', !custom);
if (custom) { document.getElementById('set-home-page-custom').focus(); }
};
homeSel.addEventListener('change', syncHome);
}

document.getElementById('cms-settings-save').addEventListener('click', function () {
var btn = this;
fb.innerHTML = '';
Expand Down
7 changes: 6 additions & 1 deletion tests/Integration/Cms/FormsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,12 @@ public function settings_form_requires_a_site_name_and_lists_published_pages_as_
$this->assertTrue($form->getElement('site_name')->isRequired());
$home = $form->getElement('home_page')->getMultiOptions();
$this->assertArrayHasKey('', $home, 'the built-in landing is the empty option');
$this->assertContains('Home (en)', $home, 'a published page is a home-page choice');

// The list is now GROUPED (content pages / module pages / custom path), so a page label sits
// one level down inside its optgroup rather than at the top level. Flatten before asserting.
$labels = [];
array_walk_recursive($home, static function ($v) use (&$labels) { $labels[] = $v; });
$this->assertContains('Home (en)', $labels, 'a published page is a home-page choice');

$this->assertTrue($form->isValid(['site_name' => 'My Site', 'home_page' => '']));
$bad = new Cms_Form_Settings();
Expand Down
Loading
Loading