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
11 changes: 11 additions & 0 deletions public/.htaccess
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]

# WordPress Content Directories Are Not Pages...
#
# A directory, or the empty index.php WordPress ships in some of them
# ("Silence is golden"), answered a blank 200 or a 403: either way it told
# a visitor the directory exists. The front controller answers instead,
# with the site's own 404. Files in them (CSS, JS, images) are served as
# before.
RewriteCond %{REQUEST_FILENAME} -d [OR]
RewriteCond %{REQUEST_FILENAME} /index\.php$
RewriteRule ^(cms/wp-content|content)(/|$) index.php [L]

# Redirect Trailing Slashes If Not A Folder...
#
# Only GET and HEAD: a redirected POST or OPTIONS is lost. The REST API is
Expand Down
61 changes: 61 additions & 0 deletions tests/install/checks.php
Original file line number Diff line number Diff line change
Expand Up @@ -691,3 +691,64 @@ function checkViewPathPrecedence(): void
: "the archive answered 200 with {$bytes} bytes — the theme root's index.php stub rendered";
});
}

// ─────────────────────────────────────────────────────────────────────────────
// 1.9 — WordPress content directories answer 404 (Pollora/framework#294)
// ─────────────────────────────────────────────────────────────────────────────

/**
* A directory under the content paths is not a page.
*
* WordPress ships an empty index.php ("Silence is golden") in wp-content,
* wp-content/plugins and wp-content/themes. Apache served it directly: a blank
* 200, which told anyone probing that the directory exists. A directory without
* one answered 403, which said the same. The skeleton's .htaccess now sends
* both to the front controller, so the site's own 404 answers — and a file in
* those directories must still be served, or every plugin's CSS would break.
*/
function checkContentDirectories(): void
{
section('1.9 — Content directories answer 404');

$directories = [
'/cms/wp-content/',
'/cms/wp-content/index.php',
'/cms/wp-content/plugins/',
'/cms/wp-content/themes/',
'/content/',
'/content/plugins/',
'/content/uploads/',
];

foreach ($directories as $path) {
test("{$path} answers the site's 404", function () use ($path) {
$response = http(siteUrl($path));

if ($response['status'] === 200 && trim($response['body']) === '') {
return "a blank 200 — WordPress's empty index.php was served directly";
}

return rendersHtml($response, 404);
});
}

test('A file in a content directory is still served', function () {
$response = http(siteUrl('/cms/wp-includes/css/dashicons.min.css'));

if ($response['status'] !== 200) {
return "a core stylesheet answered {$response['status']}";
}

// The rule is scoped to the content directories; a stylesheet in one of
// them must not be caught either.
$plugin = wpEval('$p = glob(WP_PLUGIN_DIR . "/*/*.css") ?: glob(WP_PLUGIN_DIR . "/*/*/*.css") ?: glob(WP_PLUGIN_DIR . "/*/*/*/*.css"); echo $p ? plugins_url(basename($p[0]), $p[0]) : "";');

if ($plugin === '') {
return true;
}

$status = http($plugin)['status'];

return $status === 200 ? true : "a plugin stylesheet ({$plugin}) answered {$status}";
});
}
12 changes: 9 additions & 3 deletions tests/install/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* checks run the checks against the site as it stands, without
* reinstalling; seeds install-test* fixtures, drops nothing.
* Takes an optional group — rendering, rewrites, theme,
* updates or views — to run just that one
* updates, views or directories — to run just that one
*
* Every scenario but `checks` drops the database. POLLORA_INSTALL_TESTS=1 is
* required to confirm the site is disposable.
Expand Down Expand Up @@ -67,6 +67,7 @@
checkThemeResolution();
checkThemeUpdateGuard();
checkViewPathPrecedence();
checkContentDirectories();
break;

case 'no-theme':
Expand Down Expand Up @@ -112,6 +113,7 @@
checkThemeResolution();
checkThemeUpdateGuard();
checkViewPathPrecedence();
checkContentDirectories();
break;

case 'checks':
Expand All @@ -124,8 +126,8 @@
// per fix, and the full pass is far too slow for that.
$only = $argv[2] ?? null;

if ($only !== null && ! in_array($only, ['rendering', 'rewrites', 'theme', 'updates', 'views'], true)) {
fwrite(STDERR, "\nUnknown group '{$only}': expected rendering, rewrites, theme, updates or views\n\n");
if ($only !== null && ! in_array($only, ['rendering', 'rewrites', 'theme', 'updates', 'views', 'directories'], true)) {
fwrite(STDERR, "\nUnknown group '{$only}': expected rendering, rewrites, theme, updates, views or directories\n\n");
exit(2);
}

Expand All @@ -148,6 +150,10 @@
if ($only === null || $only === 'views') {
checkViewPathPrecedence();
}

if ($only === null || $only === 'directories') {
checkContentDirectories();
}
break;
}
} catch (\Throwable $e) {
Expand Down
Loading