Doing the Impossible: Laravel 5.X on Shared Hosting

Posted on Posted in Web Development

The Easy Way

If you’re lucky, you have a host that gives you access to folders outside of your document root. In many cases, such as Namecheap, the document root is by default the public_html folder. Laravel 5.X comes with the public folder, so we need to tell Laravel to change the public directory to public_html.

First, rename the public directory to public_html (if this is the document root on your hosting provider). Then, find the following line in your index.php of the public_html folder:

$app = require_once __DIR__.'/../bootstrap/app.php';

Below that line, add the $app->bind function and tell Laravel that this is our public folder.

$app = require_once __DIR__.'/../bootstrap/app.php';

// set the public path to this directory
$app->bind('path.public', function() {
    return __DIR__;
});

That’s it if you have access beyond your document root.

The Hard Way

If you don’t happen to have access beyond your document root, this means that you will have to upload Laravel into the document root folder of your host. Be advised that this is strongly discouraged due to security concerns. However, I know that sometimes there is no other way, so here is a possibility. The majority of work is handled by the .htaccess files.

First, we need to take some security precautions. So in your root (not the public) folder, edit the .htaccess file in the following way:

<IfModule mod_rewrite.c>

<IfModule mod_negotiation.c>

Options -MultiViews

</IfModule>

RewriteEngine On

# Redirect Trailing Slashes If Not A Folder...

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)/$ /$1 [L,R=301]


# Handle Front Controller...

RewriteCond %{REQUEST_FILENAME} !-d

RewriteCond %{REQUEST_FILENAME} !-f

RewriteRule ^ index.php [L]

</IfModule>

# Protect the htaccess file
<Files .env>
Order Allow,Deny
Deny from all
</Files>
<Files .env.example>
Order Allow,Deny
Deny from all
</Files>
<Files .gitattributes>
Order Allow,Deny
Deny from all
</Files>
<Files .gitignore>
Order Allow,Deny
Deny from all
</Files>
<Files .htaccess>
Order Allow,Deny
Deny from all
</Files>
<Files _ide_helper.php>
Order Allow,Deny
Deny from all
</Files>
<Files artisan>
Order Allow,Deny
Deny from all
</Files>
<Files composer.json>
Order Allow,Deny
Deny from all
</Files>
<Files composer.lock>
Order Allow,Deny
Deny from all
</Files>
<Files gulpfile.js>
Order Allow,Deny
Deny from all
</Files>
<Files package.json>
Order Allow,Deny
Deny from all
</Files>
<Files phpspec.yml>
Order Allow,Deny
Deny from all
</Files>
<Files phpunit.xml>
Order Allow,Deny
Deny from all
</Files>
<Files readme.md>
Order Allow,Deny
Deny from all
</Files>
<Files todo.txt>
Order Allow,Deny
Deny from all
</Files>
ErrorDocument 403 /404

I won’t go into detail a lot here, but the main takeaway is that it protects your files – especially your .env file – from being accessed directly. Not disallowing file access this way will make the .env file publicly available and thus put your application at risk.

Now, imagine we request www.testcompany.com. The server looks in the root directory for an index.php file, but there is none – yet. Create an index.php file in your root directory and copy the following code into it:

<?php

/**
 * Laravel - A PHP Framework For Web Artisans
 *
 * @package  Laravel
 * @author   Taylor Otwell <taylorotwell@gmail.com>
 */

$uri = urldecode(
    parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);

// This file allows us to emulate Apache's "mod_rewrite" functionality from the
// built-in PHP web server. This provides a convenient way to test a Laravel
// application without having installed a "real" web server software here.
if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) {
    return false;
}

require_once __DIR__.'/public/index.php';

(I’m not sure where I copied this code snippet from – it wasn’t coded by myself)

Now to the .htaccess file in the public folder. Edit it to this:

<IfModule mod_rewrite.c>

<IfModule mod_negotiation.c>

Options -MultiViews

</IfModule>


RewriteEngine On

# Redirect Trailing Slashes If Not A Folder...

RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule ^(.*)/$ /$1 [L,R=301]


# Handle Front Controller...

RewriteCond %{REQUEST_FILENAME} !-d

RewriteCond %{REQUEST_FILENAME} !-f

RewriteRule ^ index.php [L]

# Prevent direct access

RewriteCond %{REQUEST_FILENAME} -d

RewriteRule ^ /404 [L]

</IfModule>

Now imagine somebody would request www.testcompany.com/app – they would have access to that folder. Of course, that’s a security vulnerability. So in every folder (except the public one), add the following .htaccess to prevent direct access:

Order Deny,Allow
Deny from all
ErrorDocument 403 /404

Notice the ErrorDocument 403 /404. It disguises to anyone wanting to access any folder besides public that there even is a folder. A 403 error would give away that the application actually has been loaded into the DocumentRoot (you can still see it in the source code because the public/ folder is visible when using the asset function). Still, why give the bad guys unnecessary information?

The asset() function

You probably use Laravel’s asset() function to include js or css files. The problem here is that when the document root misses the public part, the asset function does too. An example:

asset('js/jquery.min.js');
//evalueates to: http://www.testcompany.com/js/jquery.min.js

But the directory js/ does not exist in the root, only in the public folder. Therefore, the correct way would be:

asset('public/js/jquery.min.js');
//evaluates to: http://www.testcompany.com/public/js/jquery.min.js

You can, of course, always add public/ to the asset function but if you are developing with others, that is discouraged as it taints the original behavior and might cause confusion. An easy work around is to create a helper class with an asset function like so:

    /**
     * Adds the /public path to the assets for shared hosting
     * @param string $path The path to the file
     * @param boolean $secure false = http:// or true = https://
     * @return string The URL 
     */
    public static function asset($path, $secure = null) {
        return asset('/public/' . $path, $secure);
    }

Wherever you use the normal asset() function, you should use Helper::asset() now.

That should be it. If there are any issues, please drop me a line in the comments. I largely recreated this example from memory from a former project, so it might not be 100% accurate anymore. If so, please post the error message you get.

Leave a Reply

Your email address will not be published. Required fields are marked *