wintercms / wintercms/winter

Halcyon FileDatasource::getAvailablePaths() misses all files behind symlinked directories

Open
#1,533 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
1.5k
Forks
246
Avg merge
19h 2m
Merged PRs (30d)
7

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/Halcyon/Datasource/FileDatasource.php at getAvailablePaths() around line 342, then compare the iterator construction in select() around line 105. Run the provided /tmp symlink reproduction and verify that getAvailablePaths() lists files beneath symlinked directories, including content/static-pages/index.htm.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.