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
!)
- Put index.php in the public folder
- Set the DocumentRoot to be the public folder
- 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 targetRewriteRule
(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:
- 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.
- 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. - 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 forCONTEXT_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.