8

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.

6
  • 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. 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. 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
  • Aside: "since I cannot because it is on a shared hosting" - Many "shared hosting" accounts allow you to create "Addon domains" (as opposed to simple "Domain aliases", which I assume is what you are using here.) "Addon domains" can be configured to point to subdomains of the main domain, which can even be outside of the main domains document root. Addon domains have their own document roots so no rewrites/redirects in .htaccess are required. (If the Addon domain points to a subdirectory inside the main domain's document root then a canonical redirect might still be required.) Commented Aug 8 at 17:47

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.

UPDATE (in response to comments): ^(www\.)?example\.com\.?$ - The regex in the first condition matches an optional trailing dot. This is to allow for fully-qualified-domain-names (FQDN) that explicitly include the trailing dot. When the trailing dot is omitted - as in most cases - a FQDN is assumed (in this case). Both resolve to the same resource on your server, since a FQDN is assumed. However, the Host header varies (with/without the trailing dot). You could simply remove the trailing \.?$ part of the regex, providing you have no conflicts with other domains. However, in your case, if you fail to account for an optional trailing dot then example.com. would potentially serve content from the root/main domain (since the rules would fail).

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.)

12
  • 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
  • 2
    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 Aug 1 at 18:45
  • 2
    @BenceSzalai "Is it expected to have a trailing dot in the %{HTTP_HOST}" - yes, it's possible. The trailing dot on the domain name explicitly indicates a fully-qualified-domain-name (FQDN). If the trailing dot is omitted (as in 99.9% of cases), a FQDN is assumed (in this case). (Aside: You could just remove the trailing \.?$ on the regex, providing there are no conflicts with your other domains.) Your server will resolve both as the same, however, the Host header in the HTTP request (ie. the value of the HTTP_HOST server variable) is different... with or without a trailing dot. Commented Aug 8 at 16:51
  • 2
    @BenceSzalai Cont... Most sites do forget about the possible trailing dot on the Host header and omit the hostname-dot from the SSL cert or fail to canonicalise the request with a 301 redirect or end up serving errors or spurious content. For example, google.com. correctly 301 redirects. https://en.wikipedia.org./wiki/Fully_qualified_domain_name returns the same content as without the dot. apple.com. results in a SSL cert warning and unfriendly error message. https://www.apple.com./ (www prefix) results in an unstyled page!? (I'll update my answer to include mention of the FQDN.) Commented Aug 8 at 17:02
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 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.