Winter CMS Build
1.2 (winter/storm v1.2.14; still present on storm develop: FileDatasource.php L342)
(Filed here because issues are disabled on wintercms/storm — the affected code lives there.)
PHP Version
8.3 / 8.4
Description
Winter\Storm\Halcyon\Datasource\FileDatasource::getAvailablePaths() enumerates files with:
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->basePath));
Without FilesystemIterator::FOLLOW_SYMLINKS, SPL treats a symlinked directory as a leaf (RecursiveDirectoryIterator::hasChildren() returns false for links), so nothing beneath a symlinked directory is ever listed.
Impact
On zero-downtime deployment layouts (Laravel Forge / Envoyer style), persistent theme directories such as themes/<theme>/content and themes/<theme>/meta are typically symlinks into a shared/ directory. getAvailablePaths() feeds Cms\Classes\AutoDatasource's path cache, and every child theme (a theme with parent: in theme.yaml) resolves through an AutoDatasource — so on such a layout a child theme sees zero static pages / content and every Winter.Pages URL 404s.
The failure is hard to trace because:
- Parentless themes are unaffected: plain
FileDatasource::select() opens the scanned directory by path, and path traversal through a symlinked ancestor works fine — so the same content renders on one theme and 404s on its child.
Cms\Classes\AutoDatasource::fetchPathCache() stores the (empty) listing with Cache::rememberForever() when app.debug is off, making the empty result sticky even after the content appears.
The same flag-less construction exists in select() (L105); it only escapes the problem when the symlink sits above the scanned directory, and would skip a symlinked subdirectory inside it.
Steps to replicate
mkdir -p /tmp/shared/content/static-pages /tmp/theme/pages
echo x > /tmp/shared/content/static-pages/index.htm
echo x > /tmp/theme/pages/home.htm
ln -s /tmp/shared/content /tmp/theme/content
$ds = new Winter\Storm\Halcyon\Datasource\FileDatasource('/tmp/theme', new Winter\Storm\Filesystem\Filesystem());
var_dump(array_keys($ds->getAvailablePaths()));
// actual: ['pages/home.htm']
// expected: ['pages/home.htm', 'content/static-pages/index.htm']
Suggested fix
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
$this->basePath,
FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::SKIP_DOTS
));
We are running this as an app-level override (a FileDatasource subclass swapped in via the cms.theme.registerHalcyonDatasource event) and it resolves the issue; happy to turn it into a PR if the approach is acceptable.
Winter CMS Build
1.2 (winter/storm v1.2.14; still present on storm
develop:FileDatasource.phpL342)(Filed here because issues are disabled on wintercms/storm — the affected code lives there.)
PHP Version
8.3 / 8.4
Description
Winter\Storm\Halcyon\Datasource\FileDatasource::getAvailablePaths()enumerates files with:Without
FilesystemIterator::FOLLOW_SYMLINKS, SPL treats a symlinked directory as a leaf (RecursiveDirectoryIterator::hasChildren()returnsfalsefor links), so nothing beneath a symlinked directory is ever listed.Impact
On zero-downtime deployment layouts (Laravel Forge / Envoyer style), persistent theme directories such as
themes/<theme>/contentandthemes/<theme>/metaare typically symlinks into ashared/directory.getAvailablePaths()feedsCms\Classes\AutoDatasource's path cache, and every child theme (a theme withparent:intheme.yaml) resolves through an AutoDatasource — so on such a layout a child theme sees zero static pages / content and every Winter.Pages URL 404s.The failure is hard to trace because:
FileDatasource::select()opens the scanned directory by path, and path traversal through a symlinked ancestor works fine — so the same content renders on one theme and 404s on its child.Cms\Classes\AutoDatasource::fetchPathCache()stores the (empty) listing withCache::rememberForever()whenapp.debugis off, making the empty result sticky even after the content appears.The same flag-less construction exists in
select()(L105); it only escapes the problem when the symlink sits above the scanned directory, and would skip a symlinked subdirectory inside it.Steps to replicate
Suggested fix
We are running this as an app-level override (a
FileDatasourcesubclass swapped in via thecms.theme.registerHalcyonDatasourceevent) and it resolves the issue; happy to turn it into a PR if the approach is acceptable.