Check if file exists in subdirectory and otherwise redirect everything to main controller

Solution 1:

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.

Solution 2:

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.

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