3

What I'd like to do with mod_rewrite in a .htaccess file under Apache is the following:

  • If a request for any file or path, e.g. foo.txt or foo/bar, is received, check if such file exists in the public subdirectory, e.g. public/foo.txt or public/foo/bar. It if does, just display that file.
  • Otherwise
    • Redirect everything to the main controller index.php

My attempt at a solution was this:

<IfModule mod_rewrite.c>

    RewriteEngine On

    # Don't rewrite requests for files in the 'public' directory
    RewriteRule ^(public)($|/) - [L]

    # For all other files first check if they exist in 'public'
    RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f
    RewriteRule ^ public%{REQUEST_URI} [L]

    # Let 'index.php' handle everything else
    RewriteRule . index.php [L]

</IfModule>

Unfortunately, there is a major flaw: It doesn't work in subdirectories, i.e. when the .htaccess and all the other files are moved from / to /sub/. Ideally, it would work in arbitrary subfolders, of course.

Can anybody help? How can I fix the .htaccess to eliminate that flaw?

8
  • .htaccess only works in the current folder apache is trying to serve files from. So [1] cannot be solved, you'll need a global set of rewrite rules on you webserver configuration.
    – Marcel
    Jun 13, 2016 at 15:51
  • For your second problem, you don't have a condition that matches the request to /foo.txt, so your current rewrite rules will never treat those requests.
    – Marcel
    Jun 13, 2016 at 15:53
  • @Marcel Regarding (1), there may be a misunderstanding. But otherwise, I don't see why the .htaccess couldn't work when the entire site is moved into a subdirectory, e.g. /foo/, instead of just / (the root directory). If you write a simple rule such as RewriteRule . index.php [L], it will also work in both cases. Just not the more complex rule set from the question.
    – caw
    Jun 14, 2016 at 12:30
  • @Marcel Regarding (2), there is a rule that matches these requests. It's the RewriteRule . index.php [L] rule that matches every request. I've changed the question to drop the second problem. It's really just the issue of root directory vs subdirectories now.
    – caw
    Jun 14, 2016 at 14:39
  • but it will only work inside a folder? so how come it will work for /foo.txt?
    – Marcel
    Jun 14, 2016 at 15:26

4 Answers 4

3

I had a similar problem where I want to distribute a web app that behaves as you describe: there are a bunch of static files in a specific directory that can be accessed as if they are on the web-app root (i.e. trying /images/pic.png get the image in public/images/pic.png) , and everything else gets mapped to a controller script.

The first problem I encountered is that -f in RewriteCond doesn't work with a relative path - i.e. if the "TestPattern" string can be resolved to a file relative to the directory where the condition is specified (using .htaccess), -f will still return "not matched"(1).

So we need to try to "understand" what is the per-directory prefix that the .htaccess file is in, and mod_rewrite actually resolves this for us and makes sure that the RewriteRule is always evaluated relative to the per-directory prefix .htaccess file - but unfortunately it doesn't expose that information in anyway that RewriteCond can access (they really should let us have that).

That being said, apparently there are few weird things you can do with RewriteCond to force the "CondPattern" to tease out the relationship between %{REQUEST_URI} and the RewriteRule input - read the answers to this question for all the weird things that you can achieve.

But shortening these tricks just to the problem at hand (getting -f to match to the correct path relative to .htaccess, regardless where it might be) we get this, rather simple, thing:

RewriteEngine On

RewriteCond %{REQUEST_URI}::$1 ^(.*?/)(.*)::\2
RewriteCond %{DOCUMENT_ROOT}%1static/%2 -f
RewriteRule ^(.*)$  static/$1 [END]

RewriteRule . index.php [END,QSA]

Explanation:

The regular expression in the RewriteCond is very limited in that it isn't resolved for variables and captured text, but it can use back references to itself using the \<number> syntax. So we first create a string made up of:

  • The original request URI
  • A hard-coded delimited that we don't expect to occur in the URL (the :: part).
  • The local (per-directory) part of the URI that RewriteRule will match on - this is the $1 part: RewriteCond can look at the capture group of the target RewriteRule(2), so we capture the entire input.

We then apply a regular expression that explains the relationship between that request URI and the RewriteRule input - the request URI is made up of two parts (the first and second captures) where the first ends with a / and the second is identical to what appears after the :: delimiter - the \2 is just referencing the second capture in the same regexp, and saying that it must be identical.

The result is that we have broken up the request URI into two capture groups - the first one is the local per-directory prefix that mod_rewrite enjoys privately and the second is the local path under that directory that we want to check. The second conditions uses %<number> to refer to these captures building an absolute path under the document root(3). Note that %1 captures both the leading and trailing slashes of the prefix URI.

Finally, note that I'm using the END flag instead of L (last), as it is faster and exists mod_rewrite immediately. This allows me to drop the line with the RewriteRule … - [L] which exists just to stop the behavior of L that does not actually stop processing: it just goes back the beginning or the rewrite rules with the new URL. END is much simpler.

Footnotes:

  1. Actually, relative path resolution does work under certain conditions, as documented in the bug report for this problem. It just that it doesn't work as expected and in most common configurations will not work at all.
  2. This weird-looking backwards behavior is actually pretty simple: Apache always first resolves the RewriteRule and only then check if the conditions allow it to be applied.
  3. This, BTW, won't work if you use Alias or similar methods to map parts of the server URI space to other places not under the document root. See the documentation for CONTEXT_DOCUMENT_ROOT to figure out how to workaround this problem.
3
  • 1
    Wow, thanks a lot for this detailed walkthrough, and for the references to the related question (sometimes hard to find) and the Apache issue. It’s always great to have someone else with the same problem (Sorry!)
    – caw
    May 17, 2020 at 14:41
  • @caw - sure, and good luck if this answer works for you, please accept it and upvote. Thanks 😊
    – Guss
    May 17, 2020 at 21:23
  • 1
    Sorry, totally forgot (and didn’t see) that there was actually no selected answer yet after all those years. As for upvoting, it’s a shame, but I’d need 2 additional reputation points to do that :(
    – caw
    May 17, 2020 at 23:09
4
+50

It doesn't seem that you want to access anything outside of the /public folder? If so, this seems easy to me, and without any rewrites (I hope there isn't any reason you actually want to use mod_rewrite!)

  1. Put index.php in the public folder
  2. Set the DocumentRoot to be the public folder
  3. Use ErrorDocument to redirect any file not found to index.php

If you have a reason to use the public folder, then you can use mod_rewrite to redirect anything except index.php to public/anything, and still use ErrorDocument to redirect to index.php.

3

This ruleset works for me. I omitted the check for paths that start /public.

RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f
RewriteRule ^/(.*) /public/$1 [L]

RewriteRule . /index.html [L]

If /public is not in you document root, you can replacing %{DOCUMENT_ROOT} with the on disk path to the /public directory. The CONTEXT_DOCUMENT_ROOT may contain the file path to the content directory for the .htaccess file Unless you have the same file in /public/public as in /public, the RewriteCond won't match. I avoided this whole issue by using my equivalent of /public as my docroot. If you can't put index.php in /public, you can use Alias to access it where it is.

Your approach involves rewriting all matches. An alternate ruleset using ../public as your document root would be:

Alias /index.php /var/www/index.php RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L]

You could also handle the missing files with a custom 404 error page.

3
  • Thanks! Unfortunately, it does not work for me. Maybe someone else can test as well and verify, but on my Apache test server, these rules did only produce 404 errors (in a subdirectory) and 500 errors (in the root directory). Apart from that, the first rule (that you omitted) was important when I tried these rules because any request for public/* is valid already and should not be rewritten again (which would result in public/public/*).
    – caw
    Jun 19, 2016 at 5:12
  • @MarcoW. Updated response.
    – BillThor
    Jun 19, 2016 at 14:56
  • DOCUMENT_ROOT may be set to something not entirely correct. In my case it was /var/www whereas the real path should have been /var/www/example.com/web. This can be fixed by manually specifying the full absolute document root in RewriteCond.
    – blade
    Mar 25, 2017 at 11:19
0

Try using RewriteBase when you move it to a subfolder:

RewriteBase /subfolder/
# Rules
1
  • Thanks! I wish that worked, but it doesn't. That was the first thing I tried. But it has no effect at all, unfortunately. The problem is rather with the %{DOCUMENT_ROOT} and %{REQUEST_URI} variables that obviously have different values in the subfolder and thus stop the rules from working correctly.
    – caw
    Jun 17, 2016 at 18:32

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .