7

My intention is to serve a website from a subfolder without changing the document root to point at that folder (since I cannot because it is on a shared hosting).

For this purpose I added this to my .htaccess in the docroot:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteCond %{HTTP_HOST} ^(www\.)?example.com$
    RewriteRule !^subfolder/ /subfolder%{REQUEST_URI} [L]
</IfModule>

Since this is a WordPress site I have the standard WP .htaccess in /subfolder/, that is:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

It works well in most cases, but not when I try to use a URL that points at a folder without a trailing slash.

Symptom one:

If I open https://example.com/wp-admin it redirects me to https://example.com/wp-login.php?redirect_to=https://example.com/subfolder/wp-admin/&reauth=1, which is wrong, but if I open https://example.com/wp-admin/ (with trailing slash) it redirects me to https://example.com/wp-login.php?redirect_to=https://example.com/wp-admin/&reauth=1 which is good. This internally all comes down to in the first case $_SERVER['REQUEST_URI'] gets the value of: /subfolder/wp-admin/, but in the second case it is /wp-admin/. Obviously I want the second to be the case even if I type the URL without a trailing slash.

Symptom two:

But to minimise the components involved in the hope of narrowing down the issue I added a /subfolder/test/index.html file to the server. Now if I open https://example.com/test it redirects my browser to https://example.com/subfolder/test/ which is not the desired behaviour, but if I open https://example.com/test/ (note the trailing slash added) the browser ends up on the address https://example.com/test/, the desired behaviour. The contents of the /subfolder/test/index.html file is served in both cases.

My goal is indeed how to make it work without trailing slashed URLs, but my primary question is about why is this happening? For example I could try to put in place another redirect to redirect every request matching a folder to a triling-slashed URL variant if not having a trailing slash, probably it'll be my workaround, but that still does not give me the understanding of what is going on and prevent me from similar issues in the future as well.

4
  • 1
    "My intention is to serve a website from a subfolder without changing the document root to point at that folder" - but make it look like the website is served from the document root.
    – MrWhite
    Commented Jul 30 at 13:32
  • @MrWhite I think it is actually served from the subfolder, though for any WordPress route double rewrite occurs: /route/index.php/subfolder/index.php Commented Jul 30 at 15:04
  • @IvanShatsky Yes, my point was just clarifying that /subfolder/ does not appear in the visible URL, to make it "look like" it is served from the root, since the OP did not explicitly state this in the opening description. Yes, the "double rewrite" is unnecessary (I see you've mentioned this in your answer; as have I) - only issue with manually correcting this is if they are allowing WordPress to manage this part of the .htaccess file.
    – MrWhite
    Commented Jul 30 at 16:55
  • Yes, sorry, definitely: my goal is to hide this subfolder from the external world completely. Commented Jul 30 at 17:16

3 Answers 3

8

This is because of mod_dir and the DirectorySlash. When requesting a directory without the trailing slash, mod_dir attempts to "fix" the URL by appending a trailing slash with a 301 redirect. Unfortunately, in your scenario, the 301 redirect is occurring after the internal rewrite to /subfolder/... so the "subfolder" is exposed in the external redirect.

For example, requesting /wp-admin (no trailing slash)

  1. The root .htaccess file internally rewrites the request to /subfolder/wp-admin
  2. /subfolder/wp-admin matches a physical filesystem directory so mod_dir issues a 301 external redirect to /subfolder/wp-admin/, exposing the subfolder to the client.

If, however, you request /wp-admin/ (with a trailing slash) then the root .htaccess rewrites the request to /subfolder/wp-admin/. This maps to a physical directory and already has a trailing slash so no further redirect occurs.

It's the same redirect as when requesting any filesystem directory without the trailing slash. (eg. request /subfolder/test directly and you will be redirected.) The trailing slash on directories is required by Apache in order to correctly process .htaccess files that might be in that directory and serve DirectoryIndex documents (also handled by mod_dir).

Solution

Manually append the trailing slash to the original request if it would map to a physical directory in /subfolder.

For example:

# /.htaccess (root .htaccess file)

RewriteEngine On

# Check if a request that omits the trailing slash maps to a directory in /subfolder
# If yes then issue a redirect to append a trailing slash to the requested URL
# The 2nd condition excludes requests that look-like files
RewriteCond %{HTTP_HOST} ^(www\.)?example\.com\.?$ [NC]
RewriteCond $1 !\.\w{2,4}$
RewriteCond %{DOCUMENT_ROOT}/subfolder/$1 -d
RewriteRule (.+[^/])$ /$1/ [R=301,L]

# Rewrite all requests to /subfolder
RewriteCond %{HTTP_HOST} ^(www\.)?example\.com\.?$ [NC]
RewriteRule (.*) subfolder/$1 [L]

Assuming you have multiple domains and only example.com should be rewritten to the /subfolder. Otherwise, the HTTP_HOST condition on both rules can be removed.

The 2nd condition (RewriteCond $1 !\.\w{2,4}$) is just an optimisation to avoid requests that look-like files (ie. have file extensions) from being tested. (Filesystem checks are relatively expensive.)

No need to check that the request does not already start with /subfolder/ since if the request did start with /subfolder/ then the mod_rewrite directives in the /subfolder/.htaccess file would (by default) catch the request and completely override the mod_rewrite directives in the parent directory.

No need for the RewriteBase directive, or <IfModule> wrapper since these directives are mandatory.

Aside (additional)

HOWEVER, this is still not complete. A user could potentially access /subfolder/ directly. To prevent this, you can add a rule/redirect to the top of the /subfolder/.htaccess file that redirects the user back to root (only if this directory has been requested directly). For example:

# /subfolder/.htaccess

# Redirect any direct requests back to root
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule (.*) /$1 [R=301,L]

# Remaining WordPress directives follow...
# :

The REDIRECT_STATUS environment variable is empty on direct requests and set to 200 (as in 200 OK HTTP status) when the request is internally rewritten from root to the subdirectory.

On casual glance, the RewriteRule (.*) /$1 directive might look like it is rewriting to back itself (a loop) until you realise this .htaccess file is in a subdirectory and the RewriteRule pattern matches a relative URL-path (relative to the directory that contains the .htaccess). So, it redirects /subfolder/<anything> to /<anything>, since only <anything> is captured in the $1 backreference.

Although, strictly speaking the WordPress code block should also be modified to prevent requests being "unnecessarily" rewritten back to /index.php in the document root (to then be forwarded again to the /subfolder). This involves removing the RewriteBase directive and the slash prefix on the RewriteRule substitution string. For example:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
#RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]
</IfModule>

However, if you are allowing WordPress to modify this section of the .htaccess file then you should probably avoid this last step. (Although it is arguably preferable to prevent WordPress from managing the file for you.)

6
  • Wow, what a brilliant answer. Thank you very much! Meanwhile I've just added a redirect to any requests matching a folder inside the .htaccess inside subfolder, because I failed to come up with the proper RewriteCond %{DOCUMENT_ROOT}/subfolder/$1 -d condition you wrote, but it is better to be on the parent level, so I'll try yours. Also thanks for suggesting to optimize the WP rules. I am beyond happy to have such insightful response! Thank you! Commented Jul 30 at 17:08
  • Yes, I have multiple domains. My goal is to have all of them in their own folder. This is usually trivial for addon domains, as cPanel asks for custom document root when adding domaind, but I never seen a shared hosting where I could set the docroot of the main domain unfortunately. Commented Jul 30 at 17:20
  • +1 for "redirecting any direct requests back to root" .htaccess snippet. I thought about mentioning it, but I wasn't sure off the top of my head how to put it in effectively. Commented Jul 30 at 21:14
  • @MrWhite: you wrote RewriteCond %{HTTP_HOST} ^(www\.)?example\.com\.?$ [NC] - Is it expected to have a trailing dot in the %{HTTP_HOST} value? I never seen such case, I am always matching against the domain without an optional trailing slash. Am I missing a point? (pun not intended, but left as it is anyway) Commented yesterday
  • As per preventing WP from managing .htaccess, even if kept enabled, one line of PHP can take care of the optimizations: add_filter( 'mod_rewrite_rules', function( $rules ) { return str_replace([ 'RewriteBase /', 'RewriteRule . /index.php [L]' ], [ '', 'RewriteRule . index.php [L]' ], $rules); } );, it can be added as a MU plugin for example. Commented yesterday
4

As already pointed out by @MrWhite, it's mod_dir that redirects you to /subfolder/wp-admin/ after your root .htaccess rewrite rules rewrite the /wp-admin request URI to /subfolder/wp-admin.

I don't recommend turning off DirectorySlash, as it affects how DirectoryIndex is handled.

Instead, you can take over mod_dir's responsibility and append the trailing slash yourself when a request without a slash matches an existing directory inside the subfolder directory:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteCond %{HTTP_HOST} ^(www\.)?example.com$
    RewriteCond %{REQUEST_URI} !^subfolder/
    RewriteCond %{DOCUMENT_ROOT}/subfolder%{REQUEST_URI} -d
    RewriteRule [^/]$ %{REQUEST_URI}/ [L,QSA,R=301]
    RewriteCond %{HTTP_HOST} ^(www\.)?example.com$
    RewriteRule !^subfolder/ /subfolder%{REQUEST_URI} [L]
</IfModule>

For any WordPress route, your .htaccess file in the subfolder directory, together with your root .htaccess, will cause a double rewrite: /route/index.php/subfolder/index.php. It's better to adjust it as follows:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
    RewriteBase /subfolder/
    RewriteRule ^index\.php$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^ index.php [L,QSA]
</IfModule>
2
  • "I don't recommend turning off DirectorySlash," - Yes, I realised when continuing with my answer that this was not necessary. +1
    – MrWhite
    Commented Jul 30 at 16:21
  • Thank you! Accepted answer went to MrWhite for the lots of explanation, but thank you indeed! Commented Jul 30 at 17:28
0

The URL with a slash will behave differently to one without. But you are incharge of which URLs are used internally to reference pages and which URLs get published.

On mine

  • example.com/foo maps to example.com/foo.php
  • example.com/foo/ maps to example.com/foo/index.html if it exists otherwise index.php

The point is, it isnt a problem. Select the urls that make sense

1
  • 1
    People can type URLs, such as with /wp-admin so this is not a solution to the root problem, it is more like an attempt to minimise the surface the issue is visible on, but it is not sufficient. Thanks anyway! Commented Jul 30 at 17:09

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.