Started working on an install experience for the application

This commit is contained in:
Andy Heathershaw 2016-09-21 12:10:37 +01:00
parent 63e341199b
commit 42fb700c41
18 changed files with 538 additions and 59 deletions

View File

@ -32,4 +32,9 @@ class Configuration extends Model
* @var string
*/
protected $table = 'configuration';
public static function installCompleted()
{
return (!is_null(Configuration::where('key', 'install_completed')->first()));
}
}

View File

@ -68,7 +68,8 @@ class ConfigHelper
'sender_address' => sprintf('hostmaster@%s', (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost')),
'sender_name' => (is_null($currentAppName) ? trans('global.app_name') : $currentAppName),
'smtp_server' => 'localhost',
'smtp_port' => 25
'smtp_port' => 25,
'theme' => 'bootstrap3'
);
}
@ -78,8 +79,13 @@ class ConfigHelper
if (is_null($config))
{
$defaults = $this->defaults();
return ($defaultIfUnset && isset($defaults[$key]) ? $defaults[$key] : null);
if ($defaultIfUnset)
{
$defaults = $this->defaults();
return (isset($defaults[$key]) ? $defaults[$key] : null);
}
return null;
}
return $config->value;

View File

@ -3,6 +3,7 @@
namespace App\Helpers;
use App\Configuration;
use App\Facade\UserConfig;
class ThemeHelper
{
@ -76,15 +77,7 @@ class ThemeHelper
private function getThemeName()
{
$themeName = ThemeHelper::DEFAULT_THEME;
$currentTheme = Configuration::where('key', 'theme')->first();
if (!is_null($currentTheme))
{
$themeName = $currentTheme->value;
}
return $this->sanitiseThemeName($themeName);
return $this->sanitiseThemeName(UserConfig::get('theme'));
}
private function sanitiseThemeName($themeName)

View File

@ -4,10 +4,13 @@ namespace App\Http\Controllers;
use App\User;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class Controller extends BaseController
{

View File

@ -0,0 +1,130 @@
<?php
namespace App\Http\Controllers;
use App\Configuration;
use App\Helpers\MiscHelper;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
class InstallController extends Controller
{
public function start(Request $request)
{
if (!$this->isDatabaseWorking())
{
return $this->databaseConnection($request);
}
else if (!$this->isAdministratorAccountCreated())
{
return $this->administratorAccount($request);
}
}
protected function administratorAccount(Request $request)
{
if (strtolower($request->method()) == 'post')
{
$user = new User();
$user->name = $request->get('name');
$user->email = $request->get('email');
$user->password = bcrypt($request->get('password'));
$user->is_admin = true;
$user->is_activated = true;
$user->save();
$request->session()->flash('', '');
return redirect(route('home'));
}
return view('install/administrator');
}
protected function databaseConnection(Request $request)
{
if (strtolower($request->method()) == 'post')
{
$baseDirectory = dirname(dirname(dirname(__DIR__)));
$data = [
'APP_KEY=' . config('app.key'),
'DB_CONNECTION=mysql',
'DB_HOST=' . $request->get('mysql-server'),
'DB_PORT=' . intval($request->get('mysql-port')),
'DB_DATABASE=' . $request->get('mysql-database'),
'DB_USERNAME=' . $request->get('mysql-username'),
'DB_PASSWORD=' . $request->get('mysql-password')
];
file_put_contents(sprintf('%s/.env', $baseDirectory), join(PHP_EOL, $data) . PHP_EOL);
// Update the in-memory configuration ready for Artisan :)
$configData = [
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => $request->get('mysql-server'),
'port' => intval($request->get('mysql-port')),
'database' => $request->get('mysql-database'),
'username' => $request->get('mysql-username'),
'password' => $request->get('mysql-password')
]
]
];
config(['database' => array_replace_recursive(config('database'), $configData)]);
DB::reconnect();
try
{
Artisan::call('cache:clear');
Artisan::call('migrate', ['--force' => true]);
$result = Configuration::where('key', 'install_date')->first();
if (is_null($result))
{
$result = new Configuration();
$result->key = 'install_completed';
$result->value = true;
$result->save();
}
return redirect('/install');
}
catch (\Exception $ex)
{
$request->session()->flash('database_error', $ex->getMessage());
return redirect('/install')
->withInput($request->input());
}
}
return view('install/database', [
'database_error' => $request->session()->get('database_error')
]);
}
private function isAdministratorAccountCreated()
{
return (count(User::administrators()) > 0);
}
private function isDatabaseWorking()
{
try
{
if (!Configuration::installCompleted())
{
throw new \Exception(sprintf('install_completed configuration record is not present!'));
}
return true;
}
catch (\Exception $ex)
{
return false;
}
}
}

View File

@ -3,13 +3,16 @@
namespace App\Providers;
use App\Album;
use App\Configuration;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\ConfigHelper;
use App\Helpers\ImageHelper;
use App\Helpers\MiscHelper;
use App\Helpers\ThemeHelper;
use Illuminate\Database\QueryException;
use Illuminate\Mail\Mailer;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@ -22,28 +25,31 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->app->singleton('image', function($app)
if ($this->checkIfInstalled())
{
return new ImageHelper();
});
$this->app->singleton('theme', function($app)
{
return new ThemeHelper();
});
$this->app->singleton('user_config', function($app)
{
return new ConfigHelper();
});
$this->app->singleton('image', function ($app)
{
return new ImageHelper();
});
$this->app->singleton('theme', function ($app)
{
return new ThemeHelper();
});
$this->app->singleton('user_config', function ($app)
{
return new ConfigHelper();
});
// When running migrations or CLI tasks, don't need to add things to the view
if (php_sapi_name() != 'cli')
{
$this->addThemeInfoToView();
$this->addAlbumsToView();
// When running migrations or CLI tasks, don't need to add things to the view
if (php_sapi_name() != 'cli')
{
$this->addThemeInfoToView();
$this->addAlbumsToView();
}
// Set the default mail configuration as per user's requirements
$this->updateMailConfig();
}
// Set the default mail configuration as per user's requirements
$this->updateMailConfig();
}
/**
@ -56,6 +62,39 @@ class AppServiceProvider extends ServiceProvider
//
}
private function checkIfInstalled()
{
if ($_SERVER['REQUEST_URI'] == '/install')
{
$baseDirectory = dirname(dirname(__DIR__));
// Generate an application key and store to the .env file
if (!file_exists($baseDirectory . '/.env'))
{
$key = MiscHelper::randomString(32);
file_put_contents($baseDirectory . '/.env', sprintf('APP_KEY=%s', $key) . PHP_EOL);
app('config')->set(['app' => ['key' => $key]]);
}
return false;
}
try
{
if (!Configuration::installCompleted())
{
throw new \Exception(sprintf('install_completed configuration record is not present!'));
}
return true;
}
catch (\Exception $ex)
{
header(sprintf('Location: %s', url('/install')));
die();
}
}
private function addAlbumsToView()
{
$albums = Album::all()->sortBy('name');

View File

@ -9,15 +9,6 @@ class User extends Authenticatable
{
use Notifiable;
public static function anonymous()
{
$user = new User();
$user->id = -1;
$user->name = 'Anonymous';
return $user;
}
/**
* The attributes that are mass assignable.
*
@ -35,4 +26,18 @@ class User extends Authenticatable
protected $hidden = [
'password', 'remember_token', 'activation_token'
];
public static function administrators()
{
return User::where('is_admin', true)->get();
}
public static function anonymous()
{
$user = new User();
$user->id = -1;
$user->name = 'Anonymous';
return $user;
}
}

View File

@ -5,6 +5,11 @@
RewriteEngine On
# If not installed, redirect to install folder
RewriteCond %{DOCUMENT_ROOT}/../blue-twilight.lic !-f
RewriteCond %{REQUEST_URI} !^/upload-license.php
RewriteRule .* /upload-license.php [L,R=302]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [L,R=301]

99
public/install.php Normal file
View File

@ -0,0 +1,99 @@
<?php
namespace BtwInstaller;
use Illuminate\Support\Facades\Artisan;
use Symfony\Component\Translation\Loader\PhpFileLoader;
use Symfony\Component\Translation\Translator;
class BlueTwilightInstaller
{
private $baseDirectory;
private $licenseInfo;
public function __construct()
{
$this->baseDirectory = dirname(__DIR__);
// Display errors so installer never gets a WSOD!
ini_set('display_errors', true);
require $this->baseDirectory . '/vendor/autoload.php';
}
public function run()
{
$licenseFile = new \SplFileInfo(sprintf('%s/blue-twilight.lic', $this->baseDirectory));
if (!$licenseFile->isReadable())
{
return $this->needLicense();
}
try
{
$this->licenseInfo = $this->verifyLicense();
}
catch (\Exception $ex)
{
unlink($licenseFile->getRealPath());
return $this->needLicense();
}
$environmentFile = new \SplFileInfo(sprintf('%s/.env', $this->baseDirectory));
if (!$environmentFile->isReadable())
{
return $this->needEnvironmentSetup();
}
}
private function needEnvironmentSetup()
{
if ($_SERVER['REQUEST_METHOD'] != 'POST')
{
return $this->render('database-connection');
}
else
{
$rc = Artisan::call('migrate');
$output = Artisan::output();
var_dump($rc);
var_dump($output);
}
}
private function needLicense()
{
if ($_SERVER['REQUEST_METHOD'] != 'POST')
{
return $this->render('license');
}
else
{
move_uploaded_file($_FILES['license-file']['tmp_name'], sprintf('%s/blue-twilight.lic', $this->baseDirectory));
header(sprintf('Location: %s', $_SERVER['REQUEST_URI']), true, 302);
die();
}
}
private function render($viewName)
{
$viewName = pathinfo($viewName, PATHINFO_BASENAME);
$viewPath = sprintf('%s/resources/views/installer/%s.php', $this->baseDirectory, $viewName);
$translator = new Translator('en');
$translator->addLoader('file', new PhpFileLoader());
$translator->addResource('file', sprintf('%s/resources/lang/en/installer.php', $this->baseDirectory), 'en');
$licenseInfo = $this->licenseInfo;
require $viewPath;
}
private function verifyLicense()
{
return (require $this->baseDirectory . '/verify-license.php');
}
}
$installer = new BlueTwilightInstaller();
$installer->run();

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

41
public/upload-license.php Normal file
View File

@ -0,0 +1,41 @@
<?php
$baseDirectory = dirname(__DIR__);
$error = null;
if (strtoupper($_SERVER['REQUEST_METHOD']) == 'POST')
{
move_uploaded_file($_FILES['license-file']['tmp_name'], $baseDirectory . '/blue-twilight.lic');
try
{
$licenseInfo = (require $baseDirectory . '/verify-license.php');
if (!is_null($licenseInfo) && is_array($licenseInfo))
{
header('Location: ' . str_replace('/upload-license.php', '/', $_SERVER['REQUEST_URI']));
die();
}
}
catch (\Exception $e)
{
$error = $e->getMessage();
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Blue Twilight - License Needed</title>
</head>
<body>
<h1>Blue Twilight - License Needed</h1>
<form action="upload-license.php" method="post" enctype="multipart/form-data">
<p><label for="license-file">Upload your <u>blue-twilight.lic</u> license file:</label></p>
<p><input type="file" id="license-file" name="license-file"></p>
<p><button type="submit" style="font-size: larger;">Upload</button></p>
</form>
</body>
</html>

View File

@ -0,0 +1,12 @@
<?php
return [
'app_name' => 'Blue Twilight Installer',
'license_file_label' => 'Upload your license file:',
'license_required_text' => 'Blue Twilight requires a license file to run. The license file is called "blue-twilight.lic" ' .
'and can be downloaded from the %link_start%My Orders%link_end% page on the %link2_start%Apps by Andy - Web Store%link2_end%.',
'license_required_text2' => 'The host name in the license file must be: ',
'license_required_text3' => 'Once you have your license file, please upload it using the field below.',
'licensed_to' => 'Licensed to: ',
'save_button' => 'Save',
'upload_button' => 'Upload'
];

View File

@ -0,0 +1,37 @@
@extends('install.layout')
@section('content')
<p>You will need an administrator account to access Blue Twilight.</p>
<p>Please enter the below details to create your administrator account:</p>
<div class="row" style="margin-top: 30px;">
<div class="col-md-6 col-md-offset-3">
<form action="{{ url('/install') }}" method="post">
{{ csrf_field() }}
<div class="form-group">
<label for="name">Name:</label>
<input type="text" class="form-control" id="name" name="name" value="{{ old('name') }}" />
</div>
<div class="form-group">
<label for="email">E-mail address:</label>
<input type="email" class="form-control" id="email" name="email" value="{{ old('email') }}" />
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" class="form-control" id="password" name="password" value="{{ old('password') }}" />
</div>
<div class="form-group">
<label for="password-confirm">Confirm Password:</label>
<input type="password" class="form-control" id="password-confirm" name="password-confirm" value="{{ old('password-confirm') }}" />
</div>
<div class="form-group text-right">
<button type="submit" class="btn btn-success">Create account</button>
</div>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,47 @@
@extends('install.layout')
@section('content')
<p>Please provide the connection details to your MySQL database:</p>
<div class="row" style="margin-top: 30px;">
<div class="col-md-6 col-md-offset-3">
@if (!is_null($database_error))
<div class="alert alert-danger">
<p><strong>Database error:</strong> {{ $database_error }}</p>
</div>
@endif
<form action="{{ url('/install') }}" method="post">
{{ csrf_field() }}
<div class="form-group">
<label for="mysql-server">Server:</label>
<input type="text" class="form-control" id="mysql-server" name="mysql-server" value="{{ old('mysql-server', config('database.connections.mysql.host')) }}" />
</div>
<div class="form-group">
<label for="mysql-port">Port number:</label>
<input type="number" class="form-control" id="mysql-port" name="mysql-port" value="{{ old('mysql-port', config('database.connections.mysql.port')) }}" />
</div>
<div class="form-group">
<label for="mysql-username">Username:</label>
<input type="text" class="form-control" id="mysql-username" name="mysql-username" value="{{ old('mysql-username', config('database.connections.mysql.username')) }}" />
</div>
<div class="form-group">
<label for="mysql-password">Password:</label>
<input type="password" class="form-control" id="mysql-password" name="mysql-password" value="{{ old('mysql-password', config('database.connections.mysql.password')) }}" />
</div>
<div class="form-group">
<label for="mysql-database">Database:</label>
<input type="text" class="form-control" id="mysql-database" name="mysql-database" value="{{ old('mysql-database', config('database.connections.mysql.database')) }}" />
</div>
<div class="form-group text-right">
<button type="submit" class="btn btn-success">Save &amp; Install</button>
</div>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Blue Twilight - Installation</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<link href="themes/base/bootstrap/css/bootstrap.min.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
<style type="text/css">
html, body {
height: 100%;
}
body {
background-color: #d9edf7;
}
.container {
background-color: #fff;
height: 100%;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Install Blue Twilight</h1>
@yield('content')
</div>
</div>
</div>
<script src="themes/base/js/jquery.min.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="themes/base/bootstrap/js/bootstrap.min.js?v={{ urlencode(config('app.version')) }}"></script>
</body>
</html>

View File

@ -4,27 +4,35 @@
@section('content')
<div class="container">
<div class="row">
@foreach ($albums as $album)
<div class="col-xs-12 col-sm-4">
<div class="panel panel-default">
<div class="panel-heading"><a href="{{ $album->url() }}">{{ $album->name }}</a></div>
<div class="panel-body">
<p class="text-center">
@php($albumUrl = $album->thumbnailUrl('preview'))
@if (strlen($albumUrl) > 0)
<a href="{{ $album->url() }}"><img class="img-responsive img-rounded" src="{{ $albumUrl }}"/></a>
@endif
</p>
<p>{{ $album->description }}</p>
</div>
<div class="panel-footer">
<span class="text-left"><b>{{ $album->photos_count }}</b> photos</span>
<span class="pull-right"><a href="{{ $album->url() }}" class="btn btn-sm btn-default">Open album</a></span>
<span class="clearfix"></span>
@if (count($albums) > 0)
@foreach ($albums as $album)
<div class="col-xs-12 col-sm-4">
<div class="panel panel-default">
<div class="panel-heading"><a href="{{ $album->url() }}">{{ $album->name }}</a></div>
<div class="panel-body">
<p class="text-center">
@php($albumUrl = $album->thumbnailUrl('preview'))
@if (strlen($albumUrl) > 0)
<a href="{{ $album->url() }}"><img class="img-responsive img-rounded" src="{{ $albumUrl }}"/></a>
@endif
</p>
<p>{{ $album->description }}</p>
</div>
<div class="panel-footer">
<span class="text-left"><b>{{ $album->photos_count }}</b> photos</span>
<span class="pull-right"><a href="{{ $album->url() }}" class="btn btn-sm btn-default">Open album</a></span>
<span class="clearfix"></span>
</div>
</div>
</div>
</div>
@endforeach
@endforeach
@else
<div class="col-xs-12 text-center">
<img src="{{ $theme_url }}/images/empty-fuel-gauge.jpg" style="width: 300px;" />
<h1>Nothing to see here!</h1>
</div>
@endif
</div>
<div class="row" style="margin-top: 15px;">

View File

@ -36,6 +36,8 @@ Route::group(['prefix' => 'admin'], function () {
// Gallery
Route::get('/', 'Gallery\DefaultController@index')->name('home');
Route::get('/install', 'InstallController@start');
Route::post('/install', 'InstallController@start');
Route::get('/activate/{token}', 'Auth\ActivateController@activate')->name('auth.activate');
Route::get('{albumUrlAlias}', 'Gallery\AlbumController@index')->name('viewAlbum');
Route::get('{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@show')->name('viewPhoto');

5
verify-license.php Normal file
View File

@ -0,0 +1,5 @@
<?php
return [
'lic_name' => sg_get_const('lic_name'),
'lic_num' => sg_get_const('lic_num')
];