Skip to content

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

Description

@JonasPardon

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions