Installing and managing modules from admin panel (#847)

This commit is contained in:
Yash Govekar
2020-10-19 19:40:28 +05:30
committed by GitHub
parent ca220f1cdf
commit 5803487d51
92 changed files with 1259 additions and 669 deletions

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands;
use App\Contracts\Command;
use App\Services\ImporterService;
use Illuminate\Support\Facades\Log;
class ImportFromClassicCommand extends Command
{
protected $signature = 'phpvms:importer {db_host} {db_name} {db_user} {db_pass?} {table_prefix=phpvms_}';
protected $description = 'Import from an older version of phpVMS';
/**
* Run dev related commands
*/
public function handle()
{
$creds = [
'host' => $this->argument('db_host'),
'name' => $this->argument('db_name'),
'user' => $this->argument('db_user'),
'pass' => $this->argument('db_pass'),
'table_prefix' => $this->argument('table_prefix'),
];
$importerSvc = new ImporterService();
$importerSvc->saveCredentials($creds);
$manifest = $importerSvc->generateImportManifest();
foreach ($manifest as $record) {
try {
$importerSvc->run($record['importer'], $record['start']);
} catch (\Exception $e) {
Log::error($e->getMessage());
}
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateModulesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('modules', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->boolean('enabled')->default(1);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('modules');
}
}

View File

@@ -0,0 +1,10 @@
- name: 'Awards'
enabled: 1
- name: 'Sample'
enabled: 1
- name: 'VMSAcars'
enabled: 1
- name: 'Vacentral'
enabled: 1
- name: 'TestModule'
enabled: 1

View File

@@ -51,3 +51,6 @@
- name: maintenance
display_name: Maintenance
description: Run maintenance tasks
- name: modules
display_name: Modules
description: Add/Edit Modules

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Exceptions;
class ModuleExistsException extends AbstractHttpException
{
private $module_name;
public function __construct($module_name)
{
$this->module_name = $module_name;
parent::__construct(
409,
'Module '.$module_name.' Already Exists!'
);
}
/**
* Return the RFC 7807 error type (without the URL root)
*/
public function getErrorType(): string
{
return 'module-already-exists';
}
/**
* Get the detailed error string
*/
public function getErrorDetails(): string
{
return $this->getMessage();
}
/**
* Return an array with the error details, merged with the RFC7807 response
*/
public function getErrorMetadata(): array
{
return [];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Exceptions;
class ModuleInstallationError extends AbstractHttpException
{
public function __construct()
{
parent::__construct(
500,
'Installation of Module Failed!'
);
}
/**
* Return the RFC 7807 error type (without the URL root)
*/
public function getErrorType(): string
{
return 'module-installation-error';
}
/**
* Get the detailed error string
*/
public function getErrorDetails(): string
{
return $this->getMessage();
}
/**
* Return an array with the error details, merged with the RFC7807 response
*/
public function getErrorMetadata(): array
{
return [];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Exceptions;
class ModuleInvalidFileType extends AbstractHttpException
{
public function __construct()
{
parent::__construct(
415,
'The Module File Type is Invalid!'
);
}
/**
* Return the RFC 7807 error type (without the URL root)
*/
public function getErrorType(): string
{
return 'module-file-type-invalid';
}
/**
* Get the detailed error string
*/
public function getErrorDetails(): string
{
return $this->getMessage();
}
/**
* Return an array with the error details, merged with the RFC7807 response
*/
public function getErrorMetadata(): array
{
return [];
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Contracts\Controller;
use App\Services\ModuleService;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\View\View;
class ModulesController extends Controller
{
private $moduleSvc;
public function __construct(ModuleService $moduleSvc)
{
$this->moduleSvc = $moduleSvc;
}
/**
* Display a listing of the Module.
*
* @return mixed
*/
public function index()
{
$modules = $this->moduleSvc->getAllModules();
$new_modules = $this->moduleSvc->scan();
return view('admin.modules.index', [
'modules' => $modules,
'new_modules' => $new_modules,
]);
}
/**
* Show the form for creating a new Module.
*
* @return Application|Factory|View
*/
public function create()
{
return view('admin.modules.create');
}
/**
* Store a newly Uploaded Module in the Storage.
*
* @param Request $request
*
* @return Application|RedirectResponse|Redirector
*/
public function store(Request $request)
{
$this->moduleSvc->installModule($request->file('module_file'));
return $this->index();
}
/**
* Show the form for editing the specified Module.
*
* @param $id
*
* @return Application|Factory|View
*/
public function edit($id)
{
$module = $this->moduleSvc->getModule($id);
return view('admin.modules.edit', [
'module' => $module,
]);
}
/**
* Update the specified Module in storage.
*
* @param $id
* @param Request $request
*
* @return Application|RedirectResponse|Redirector
*/
public function update($id, Request $request)
{
$this->moduleSvc->updateModule($id, $request->has('enabled'));
flash()->success('Module Status Changed!');
return redirect(route('admin.modules.index'));
}
/**
* Enabling Module Present in the Modules Folder
*
* @param Request $request
*
* @return Application|RedirectResponse|Redirector
*/
public function enable(Request $request)
{
$this->moduleSvc->addModule($request->input('name'));
return redirect(route('admin.modules.index'));
}
/**
* Verify and Remove the specified Module from storage.
*
* @param mixed $id
* @param Request $request
*
* @return mixed
*/
public function destroy($id, Request $request)
{
$delete = $this->moduleSvc->deleteModule($id, $request->all());
if ($delete == true) {
flash()->success('Module Deleted Successfully!');
return redirect(route('admin.modules.index'));
}
flash()->error('Verification Failed!');
return redirect(route('admin.modules.edit', $id));
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\System;
use App\Contracts\Controller;
use App\Services\ImporterService;
use App\Services\Installer\DatabaseService;
use App\Support\Utils;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ImporterController extends Controller
{
private $dbSvc;
private $importerSvc;
public function __construct(DatabaseService $dbSvc, ImporterService $importerSvc)
{
$this->dbSvc = $dbSvc;
$this->importerSvc = $importerSvc;
Utils::disableDebugToolbar();
}
/**
* Show the main page for the importer; show form for the admin email
* and the credentials for the other database
*
* @return mixed
*/
public function index()
{
return view('system.importer.step1-configure');
}
protected function testDb(Request $request)
{
$this->dbSvc->checkDbConnection(
$request->post('db_conn'),
$request->post('db_host'),
$request->post('db_port'),
$request->post('db_name'),
$request->post('db_user'),
$request->post('db_pass')
);
}
/**
* Check the database connection
*
* @param Request $request
*
* @return mixed
*/
public function dbtest(Request $request)
{
$status = 'success'; // success|warn|danger
$message = 'Database connection looks good!';
try {
$this->testDb($request);
} catch (Exception $e) {
$status = 'danger';
$message = 'Failed! '.$e->getMessage();
}
return view('system.importer.dbtest', [
'status' => $status,
'message' => $message,
]);
}
/**
* The post from the above
*
* @param Request $request
*
* @return mixed
*/
public function config(Request $request)
{
try {
$this->testDb($request);
// Save the credentials to use later
$this->importerSvc->saveCredentialsFromRequest($request);
// Generate the import manifest
$manifest = $this->importerSvc->generateImportManifest();
} catch (Exception $e) {
Log::error($e->getMessage());
// Send it to run, step1
return view('system.importer.error', [
'error' => $e->getMessage(),
]);
}
// Send it to run, step1
return view('system.importer.step2-processing', [
'manifest' => $manifest,
]);
}
/**
* Run the importer. Pass in query string with a few different parameters:
*
* stage=STAGE NAME
* start=record_start
*
* @param Request $request
*
* @throws Exception
*
* @return mixed
*/
public function run(Request $request)
{
$importer = $request->input('importer');
$start = $request->input('start');
Log::info('Starting stage '.$importer.' from offset '.$start);
$this->importerSvc->run($importer, $start);
return response()->json([
'message' => 'completed',
]);
}
/**
* Complete the import
*/
public function complete()
{
return redirect('/');
}
}

View File

@@ -0,0 +1,356 @@
<?php
namespace App\Http\Controllers\System;
use App\Contracts\Controller;
use App\Services\AirlineService;
use App\Services\AnalyticsService;
use App\Services\Installer\ConfigService;
use App\Services\Installer\DatabaseService;
use App\Services\Installer\MigrationService;
use App\Services\Installer\RequirementsService;
use App\Services\Installer\SeederService;
use App\Services\UserService;
use App\Support\Countries;
use App\Support\Utils;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Database\QueryException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\View\View;
use function in_array;
use Laracasts\Flash\Flash;
use RuntimeException;
class InstallerController extends Controller
{
private $airlineSvc;
private $analyticsSvc;
private $dbSvc;
private $envSvc;
private $migrationSvc;
private $reqSvc;
private $seederSvc;
private $userService;
/**
* InstallerController constructor.
*
* @param AirlineService $airlineSvc
* @param AnalyticsService $analyticsSvc
* @param DatabaseService $dbService
* @param ConfigService $envService
* @param MigrationService $migrationSvc
* @param RequirementsService $reqSvc
* @param SeederService $seederSvc
* @param UserService $userService
*/
public function __construct(
AirlineService $airlineSvc,
AnalyticsService $analyticsSvc,
DatabaseService $dbService,
ConfigService $envService,
MigrationService $migrationSvc,
RequirementsService $reqSvc,
SeederService $seederSvc,
UserService $userService
) {
$this->airlineSvc = $airlineSvc;
$this->analyticsSvc = $analyticsSvc;
$this->dbSvc = $dbService;
$this->envSvc = $envService;
$this->migrationSvc = $migrationSvc;
$this->reqSvc = $reqSvc;
$this->seederSvc = $seederSvc;
$this->userService = $userService;
Utils::disableDebugToolbar();
}
/**
* Display a listing of the resource.
*/
public function index()
{
if (config('app.key') !== 'base64:zdgcDqu9PM8uGWCtMxd74ZqdGJIrnw812oRMmwDF6KY=') {
return view('system.installer.errors.already-installed');
}
return view('system.installer.install.index-start');
}
protected function testDb(Request $request)
{
$this->dbSvc->checkDbConnection(
$request->post('db_conn'),
$request->post('db_host'),
$request->post('db_port'),
$request->post('db_name'),
$request->post('db_user'),
$request->post('db_pass')
);
}
/**
* Check the database connection
*
* @param Request $request
*
* @return Application|Factory|View
*/
public function dbtest(Request $request)
{
$status = 'success'; // success|warn|danger
$message = 'Database connection looks good!';
try {
$this->testDb($request);
} catch (Exception $e) {
$status = 'danger';
$message = 'Failed! '.$e->getMessage();
}
return view('system.installer.install.dbtest', [
'status' => $status,
'message' => $message,
]);
}
/**
* Check if any of the items has been marked as failed
*
* @param array $arr
*
* @return bool
*/
protected function allPassed(array $arr): bool
{
foreach ($arr as $item) {
if ($item['passed'] === false) {
return false;
}
}
return true;
}
/**
* Step 1. Check the modules and permissions
*
* @return Factory|View
*/
public function step1()
{
$php_version = $this->reqSvc->checkPHPVersion();
$extensions = $this->reqSvc->checkExtensions();
$directories = $this->reqSvc->checkPermissions();
// Only pass if all the items in the ext and dirs are passed
$statuses = [
$php_version['passed'] === true,
$this->allPassed($extensions) === true,
$this->allPassed($directories) === true,
];
// Make sure there are no false values
$passed = !in_array(false, $statuses, true);
return view('system.installer.install.steps.step1-requirements', [
'php' => $php_version,
'extensions' => $extensions,
'directories' => $directories,
'passed' => $passed,
]);
}
/**
* Step 2. Database Setup
*
* @return Factory|View
*/
public function step2()
{
$db_types = ['mysql' => 'mysql', 'sqlite' => 'sqlite'];
return view('system.installer.install.steps.step2-db', [
'db_types' => $db_types,
]);
}
/**
* Step 2a. Create the .env
*
* @param Request $request
*
* @return RedirectResponse|Redirector
*/
public function envsetup(Request $request)
{
$log_str = $request->post();
$log_str['db_pass'] = '';
Log::info('ENV setup', $log_str);
// Before writing out the env file, test the DB credentials
try {
$this->testDb($request);
} catch (Exception $e) {
Log::error('Testing db before writing configs failed');
Log::error($e->getMessage());
Flash::error($e->getMessage());
return redirect(route('installer.step2'))->withInput();
}
// Now write out the env file
$attrs = [
'SITE_NAME' => $request->post('site_name'),
'SITE_URL' => $request->post('site_url'),
'DB_CONN' => $request->post('db_conn'),
'DB_HOST' => $request->post('db_host'),
'DB_PORT' => $request->post('db_port'),
'DB_NAME' => $request->post('db_name'),
'DB_USER' => $request->post('db_user'),
'DB_PASS' => $request->post('db_pass'),
'DB_PREFIX' => $request->post('db_prefix'),
];
/*
* Create the config files and then redirect so that the
* framework can pickup all those configs, etc, before we
* setup the database and stuff
*/
try {
$this->envSvc->createConfigFiles($attrs);
} catch (Exception $e) {
Log::error('Config files failed to write');
Log::error($e->getMessage());
Flash::error($e->getMessage());
return redirect(route('installer.step2'))->withInput();
}
// Needs to redirect so it can load the new .env
Log::info('Redirecting to database setup');
return redirect(route('installer.dbsetup'));
}
/**
* Step 2b. Setup the database
*
* @return mixed
*/
public function dbsetup()
{
$console_out = '';
try {
$console_out .= $this->dbSvc->setupDB();
$console_out .= $this->migrationSvc->runAllMigrations();
$this->seederSvc->syncAllSeeds();
} catch (QueryException $e) {
Log::error('Error on db setup: '.$e->getMessage());
//dd($e);
$this->envSvc->removeConfigFiles();
Flash::error($e->getMessage());
return redirect(route('installer.step2'))->withInput();
}
$console_out = trim($console_out);
return view('system.installer.install.steps.step2a-db_output', [
'console_output' => $console_out,
]);
}
/**
* Step 3. Setup the admin user and initial settings
*/
public function step3()
{
return view('system.installer.install.steps.step3-user', [
'countries' => Countries::getSelectList(),
]);
}
/**
* Step 3 submit
*
* @param Request $request
*
* @throws RuntimeException
* @throws Exception
*
* @return mixed
*/
public function usersetup(Request $request)
{
$validator = Validator::make($request->all(), [
'airline_name' => 'required',
'airline_icao' => 'required|size:3|unique:airlines,icao',
'airline_country' => 'required',
'name' => 'required',
'email' => 'required|email|unique:users,email',
'password' => 'required|confirmed',
]);
if ($validator->fails()) {
return redirect('install/step3')
->withErrors($validator)
->withInput();
}
/**
* Create the first airline
*/
$attrs = [
'icao' => $request->get('airline_icao'),
'name' => $request->get('airline_name'),
'country' => $request->get('airline_country'),
];
$airline = $this->airlineSvc->createAirline($attrs);
/**
* Create the user, and associate to the airline
* Ensure the seed data at least has one airport
* KAUS, for giggles, though.
*/
$attrs = [
'name' => $request->get('name'),
'email' => $request->get('email'),
'api_key' => Utils::generateApiKey(),
'airline_id' => $airline->id,
'password' => Hash::make($request->get('password')),
];
$user = $this->userService->createUser($attrs, ['admin']);
Log::info('User registered: ', $user->toArray());
// Set the initial admin e-mail address
setting_save('general.admin_email', $user->email);
// Save telemetry setting
setting_save('general.telemetry', get_truth_state($request->get('telemetry')));
// Try sending telemetry info
$this->analyticsSvc->sendInstall();
return view('system.installer.install.steps.step3a-completed', []);
}
/**
* Final step
*
* @return RedirectResponse|Redirector
*/
public function complete()
{
return redirect('/login');
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace App\Http\Controllers\System;
use App\Contracts\Controller;
use App\Repositories\KvpRepository;
use App\Services\AnalyticsService;
use App\Services\Installer\InstallerService;
use App\Services\Installer\MigrationService;
use App\Services\Installer\SeederService;
use Codedge\Updater\UpdaterManager;
use function count;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class UpdateController extends Controller
{
private $analyticsSvc;
private $installerSvc;
private $kvpRepo;
private $migrationSvc;
private $seederSvc;
private $updateManager;
/**
* @param AnalyticsService $analyticsSvc
* @param InstallerService $installerSvc
* @param KvpRepository $kvpRepo
* @param MigrationService $migrationSvc
* @param SeederService $seederSvc
* @param UpdaterManager $updateManager
*/
public function __construct(
AnalyticsService $analyticsSvc,
InstallerService $installerSvc,
KvpRepository $kvpRepo,
MigrationService $migrationSvc,
SeederService $seederSvc,
UpdaterManager $updateManager
) {
$this->analyticsSvc = $analyticsSvc;
$this->migrationSvc = $migrationSvc;
$this->seederSvc = $seederSvc;
$this->installerSvc = $installerSvc;
$this->kvpRepo = $kvpRepo;
$this->updateManager = $updateManager;
}
/**
* Display a listing of the resource.
*/
public function index()
{
return view('system.updater.index-start');
}
/**
* Step 1. Check if there's an update available. Check if there
* are any unrun migrations
*
* @return mixed
*/
public function step1()
{
$this->installerSvc->clearCaches();
if ($this->installerSvc->isUpgradePending()) {
Log::info('Upgrade is pending');
}
return view('system.updater.steps.step1-update-available');
}
/**
* Step 2 Run all of the migrations
*
* @param Request $request
*
* @return mixed
*/
public function run_migrations(Request $request)
{
Log::info('Update: run_migrations', $request->post());
$migrations = $this->migrationSvc->migrationsAvailable();
if (count($migrations) === 0) {
$this->seederSvc->syncAllSeeds();
return view('system.updater.steps.step3-update-complete');
}
$output = $this->migrationSvc->runAllMigrations();
$this->seederSvc->syncAllSeeds();
return view('system.updater.steps.step2-migrations-done', [
'console_output' => $output,
]);
}
/**
* Final step
*
* @return mixed
*/
public function complete()
{
return redirect('/login');
}
/**
* Show the update page with the latest version
*
* @return Factory|View
*/
public function updater()
{
$version = $this->kvpRepo->get('latest_version_tag');
return view('system.updater.downloader/downloader', [
'version' => $version,
]);
}
/**
* Download the actual update and then forward the user to the updater page
*
* @return mixed
*/
public function update_download()
{
$version = $this->kvpRepo->get('latest_version_tag');
if (empty($version)) {
return view('system.updater.steps.step1-no-update');
}
$release = $this->updateManager->source('github')->fetch($version);
$this->updateManager->source('github')->update($release);
$this->analyticsSvc->sendUpdate();
Log::info('Update completed to '.$version.', redirecting');
return redirect('/update');
}
}

28
app/Models/Module.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use App\Contracts\Model;
/**
* Class ModuleManager
*/
class Module extends Model
{
public $table = 'modules';
public $fillable = [
'name',
'enabled',
'created_at',
'updated_at',
];
protected $casts = [
'enabled' => 'boolean',
];
public static $rules = [
'name' => 'required',
];
}

View File

@@ -27,6 +27,74 @@ class RouteServiceProvider extends ServiceProvider
$this->mapWebRoutes();
$this->mapAdminRoutes();
$this->mapApiRoutes();
$this->mapImporterRoutes();
$this->mapInstallerRoutes();
$this->mapUpdaterRoutes();
}
private function mapImporterRoutes()
{
Route::group([
'as' => 'importer.',
'prefix' => 'importer',
'middleware' => ['web'],
'namespace' => 'App\Http\Controllers\System',
], function () {
Route::get('/', 'ImporterController@index')->name('index');
Route::post('/config', 'ImporterController@config')->name('config');
Route::post('/dbtest', 'ImporterController@dbtest')->name('dbtest');
// Run the actual importer process. Additional middleware
Route::post('/run', 'ImporterController@run')->middleware('api')->name('run');
Route::post('/complete', 'ImporterController@complete')->name('complete');
});
}
private function mapInstallerRoutes()
{
Route::group([
'as' => 'installer.',
'prefix' => 'install',
'middleware' => ['web'],
'namespace' => 'App\Http\Controllers\System',
], function () {
Route::get('/', 'InstallerController@index')->name('index');
Route::post('/dbtest', 'InstallerController@dbtest')->name('dbtest');
Route::get('/step1', 'InstallerController@step1')->name('step1');
Route::post('/step1', 'InstallerController@step1')->name('step1post');
Route::get('/step2', 'InstallerController@step2')->name('step2');
Route::post('/envsetup', 'InstallerController@envsetup')->name('envsetup');
Route::get('/dbsetup', 'InstallerController@dbsetup')->name('dbsetup');
Route::get('/step3', 'InstallerController@step3')->name('step3');
Route::post('/usersetup', 'InstallerController@usersetup')->name('usersetup');
Route::get('/complete', 'InstallerController@complete')->name('complete');
});
}
protected function mapUpdaterRoutes()
{
Route::group([
'as' => 'update.',
'prefix' => 'update',
'middleware' => ['web', 'auth', 'ability:admin,admin-access'],
'namespace' => 'App\Http\Controllers\System',
], function () {
Route::get('/', 'UpdateController@index')->name('index');
Route::get('/step1', 'UpdateController@step1')->name('step1');
Route::post('/step1', 'UpdateController@step1')->name('step1post');
Route::post('/run-migrations', 'UpdateController@run_migrations')->name('run_migrations');
Route::get('/complete', 'UpdateController@complete')->name('complete');
// Routes for the update downloader
Route::get('/downloader', 'UpdateController@updater')->name('updater');
Route::post('/downloader', 'UpdateController@update_download')->name('update_download');
});
}
/**
@@ -41,7 +109,7 @@ class RouteServiceProvider extends ServiceProvider
Route::group([
'middleware' => ['web'],
'namespace' => $this->namespace,
], function ($router) {
], function () {
Route::group([
'namespace' => 'Frontend',
'prefix' => '',
@@ -385,6 +453,35 @@ class RouteServiceProvider extends ServiceProvider
'delete',
], 'dashboard/news', ['uses' => 'DashboardController@news'])
->name('dashboard.news')->middleware('update_pending', 'ability:admin,admin-access');
//Modules
Route::group([
'as' => 'modules.',
'prefix' => 'modules',
'middleware' => ['ability:admin, modules'],
], function () {
//Modules Index
Route::get('/', 'ModulesController@index')->name('index');
//Add Module
Route::get('/create', 'ModulesController@create')->name('create');
//Store Module
Route::post('/create', 'ModulesController@store')->name('store');
//Enable Module
Route::post('/enable', 'ModulesController@enable')->name('enable');
//Edit Module
Route::get('/{id}/edit', 'ModulesController@edit')->name('edit');
//Update Module
Route::post('/{id}', 'ModulesController@update')->name('update');
//Delete Module
Route::delete('/{id}', 'ModulesController@destroy')->name('destroy');
});
});
}
@@ -402,7 +499,7 @@ class RouteServiceProvider extends ServiceProvider
'namespace' => $this->namespace.'\\Api',
'prefix' => 'api',
'as' => 'api.',
], function ($router) {
], function () {
Route::group([], function () {
Route::get('acars', 'AcarsController@live_flights');
Route::get('acars/geojson', 'AcarsController@pireps_geojson');

View File

@@ -0,0 +1,146 @@
<?php
namespace App\Services;
use App\Contracts\Service;
use App\Repositories\KvpRepository;
use App\Services\Importers\AircraftImporter;
use App\Services\Importers\AirlineImporter;
use App\Services\Importers\AirportImporter;
use App\Services\Importers\ClearDatabase;
use App\Services\Importers\ExpenseImporter;
use App\Services\Importers\FinalizeImporter;
use App\Services\Importers\FlightImporter;
use App\Services\Importers\GroupImporter;
use App\Services\Importers\LedgerImporter;
use App\Services\Importers\PirepImporter;
use App\Services\Importers\RankImport;
use App\Services\Importers\SettingsImporter;
use App\Services\Importers\UserImport;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ImporterService extends Service
{
private $CREDENTIALS_KEY = 'legacy.importer.db';
/**
* @var KvpRepository
*/
private $kvpRepo;
/**
* The list of importers, in proper order
*/
private $importList = [
ClearDatabase::class,
RankImport::class,
GroupImporter::class,
AirlineImporter::class,
AircraftImporter::class,
AirportImporter::class,
FlightImporter::class,
UserImport::class,
PirepImporter::class,
ExpenseImporter::class,
LedgerImporter::class,
SettingsImporter::class,
FinalizeImporter::class,
];
public function __construct()
{
$this->kvpRepo = app(KvpRepository::class);
}
/**
* Save the credentials from a request
*
* @param \Illuminate\Http\Request $request
*/
public function saveCredentialsFromRequest(Request $request)
{
$creds = [
'host' => $request->post('db_host'),
'port' => $request->post('db_port'),
'name' => $request->post('db_name'),
'user' => $request->post('db_user'),
'pass' => $request->post('db_pass'),
'table_prefix' => $request->post('db_prefix'),
];
$this->saveCredentials($creds);
}
/**
* Save the given credentials
*
* @param array $creds
*/
public function saveCredentials(array $creds)
{
$creds = array_merge([
'admin_email' => '',
'host' => '',
'port' => '',
'name' => '',
'user' => '',
'pass' => 3306,
'table_prefix' => 'phpvms_',
], $creds);
$this->kvpRepo->save($this->CREDENTIALS_KEY, $creds);
}
/**
* Get the saved credentials
*/
public function getCredentials()
{
return $this->kvpRepo->get($this->CREDENTIALS_KEY);
}
/**
* Create a manifest of the import. Creates an array with the importer name,
* which then has a subarray of all of the different steps/stages it needs to run
*/
public function generateImportManifest()
{
$manifest = [];
foreach ($this->importList as $importerKlass) {
/** @var BaseImporter $importer */
$importer = new $importerKlass();
$manifest = array_merge($manifest, $importer->getManifest());
}
return $manifest;
}
/**
* Run a given stage
*
* @param $importer
* @param int $start
*
* @throws \Exception
*
* @return int|void
*/
public function run($importer, $start = 0)
{
if (!in_array($importer, $this->importList, true)) {
throw new Exception('Unknown importer "'.$importer.'"');
}
/** @var $importerInst BaseImporter */
$importerInst = new $importer();
try {
$importerInst->run($start);
} catch (Exception $e) {
Log::error('Error running importer: '.$e->getMessage());
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Services\Importers;
use App\Models\Aircraft;
use App\Models\Airline;
use App\Models\Subfleet;
class AircraftImporter extends BaseImporter
{
protected $table = 'aircraft';
public function run($start = 0)
{
$this->comment('--- AIRCRAFT IMPORT ---');
$fields = [
'id',
'icao',
'name',
'fullname',
'registration',
'enabled',
];
// See if there is an airline column
$columns = $this->db->getColumns($this->table);
if (in_array('airline', $columns, true)) {
$fields[] = 'airline';
}
if (in_array('location', $columns, true)) {
$fields[] = 'location';
}
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start, $fields);
foreach ($rows as $row) {
$subfleet_name = $row->icao;
$airline_id = null;
if (!empty($row->airline)) {
$subfleet_name = $row->airline.' - '.$row->icao;
$airline_id = $this->idMapper->getMapping('airlines', $row->airline);
}
$subfleet = $this->getSubfleet($subfleet_name, $row->icao, $airline_id);
$where = [
'registration' => $row->registration,
];
$cols = [
'icao' => $row->icao,
'name' => $row->fullname,
'subfleet_id' => $subfleet->id,
'active' => $row->enabled,
];
if (!empty($row->location)) {
$cols['airport_id'] = $row->location;
}
$aircraft = Aircraft::firstOrCreate($where, $cols);
$this->idMapper->addMapping('aircraft', $row->id, $aircraft->id);
if ($aircraft->wasRecentlyCreated) {
$count++;
}
}
$this->info('Imported '.$count.' aircraft');
}
/**
* Return the subfleet
*
* @param string $name
* @param string $icao ICAO of the subfleet
* @param int $airline_id
*
* @return mixed
*/
protected function getSubfleet($name, $icao, $airline_id = null)
{
if (empty($airline_id)) {
$airline = Airline::first();
$airline_id = $airline->id;
}
return Subfleet::firstOrCreate([
'airline_id' => $airline_id,
'name' => $name,
], ['type' => $icao]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Services\Importers;
use App\Models\Airline;
use Illuminate\Support\Facades\Log;
class AirlineImporter extends BaseImporter
{
public $table = 'airlines';
/**
* @param int $start
*/
public function run($start = 0)
{
$this->comment('--- AIRLINE IMPORT ---');
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start);
foreach ($rows as $row) {
$attrs = [
'iata' => $row->code,
'name' => $row->name,
'active' => $row->enabled,
];
$w = ['icao' => $row->code];
//$airline = Airline::firstOrCreate($w, $attrs);
$airline = Airline::create(array_merge($w, $attrs));
$this->idMapper->addMapping('airlines', $row->id, $airline->id);
$this->idMapper->addMapping('airlines', $row->code, $airline->id);
Log::debug('Mapping '.$row->id.'/'.$row->code.' to ID '.$airline->id);
if ($airline->wasRecentlyCreated) {
$count++;
}
}
$this->info('Imported '.$count.' airlines');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Services\Importers;
use App\Models\Airport;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Log;
class AirportImporter extends BaseImporter
{
protected $table = 'airports';
public function run($start = 0)
{
$this->comment('--- AIRPORT IMPORT ---');
$fields = [
'icao',
'name',
'country',
'lat',
'lng',
'hub',
];
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start, $fields);
foreach ($rows as $row) {
$attrs = [
'id' => trim($row->icao),
'icao' => trim($row->icao),
'name' => $row->name,
'country' => $row->country,
'lat' => $row->lat,
'lon' => $row->lng,
'hub' => $row->hub,
];
$w = ['id' => $attrs['id']];
//$airport = Airport::updateOrCreate($w, $attrs);
try {
$airport = Airport::create(array_merge($w, $attrs));
} catch (QueryException $e) {
$sqlState = $e->errorInfo[0];
$errorCode = $e->errorInfo[1];
if ($sqlState === '23000' && $errorCode === 1062) {
Log::info('Found duplicate for '.$row->icao.', ignoring');
return true;
}
return false;
}
if ($airport->wasRecentlyCreated) {
$count++;
}
}
$this->info('Imported '.$count.' airports');
return true;
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace App\Services\Importers;
use App\Services\ImporterService;
use App\Services\Installer\LoggerTrait;
use App\Utils\IdMapper;
use App\Utils\ImporterDB;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
abstract class BaseImporter
{
use LoggerTrait;
use Dispatchable;
use InteractsWithQueue;
use Queueable;
/**
* Holds the connection to the legacy database
*
* @var ImporterDB
*/
protected $db;
/**
* The mapper class used for old IDs to new IDs
*
* @var IdMapper
*/
protected $idMapper;
/**
* The legacy table this importer targets
*
* @var string
*/
protected $table;
/**
* The column used for the ID, used for the ORDER BY
*
* @var string
*/
protected $idField = 'id';
public function __construct()
{
$importerService = app(ImporterService::class);
$this->db = new ImporterDB($importerService->getCredentials());
$this->idMapper = app(IdMapper::class);
}
/**
* The start method. Takes the offset to start from
*
* @param int $start
*
* @return mixed
*/
abstract public function run($start = 0);
/**
* Return a manifest of the import tasks to run. Returns an array of objects,
* which contain a start and end row
*
* @return array
*/
public function getManifest(): array
{
$manifest = [];
// Ensure that the table exists; if it doesn't skip it from the manifest
if (!$this->db->tableExists($this->table)) {
Log::info('Table '.$this->table.' doesn\'t exist');
return [];
}
$start = 0;
$total_rows = $this->db->getTotalRows($this->table);
Log::info('Found '.$total_rows.' rows for '.$this->table);
do {
$end = $start + $this->db->batchSize;
if ($end > $total_rows) {
$end = $total_rows;
}
$idx = $start + 1;
$manifest[] = [
'importer' => static::class,
'start' => $start,
'end' => $end,
'message' => 'Importing '.$this->table.' ('.$idx.' - '.$end.' of '.$total_rows.')',
];
$start += $this->db->batchSize;
} while ($start < $total_rows);
return $manifest;
}
/**
* Determine what columns exist, can be used for feature testing between v2/v5
*
* @return array
*/
public function getColumns(): array
{
return [];
}
/**
* @param $date
*
* @return Carbon
*/
protected function parseDate($date)
{
return Carbon::parse($date);
}
/**
* Take a decimal duration and convert it to minutes
*
* @param $duration
*
* @return float|int
*/
protected function convertDuration($duration)
{
if (strpos($duration, '.') !== false) {
$delim = '.';
} elseif (strpos($duration, ':')) {
$delim = ':';
} else {
// no delimiter, assume it's just a straight hour
return (int) $duration * 60;
}
$hm = explode($delim, $duration);
$hours = (int) $hm[0] * 60;
$mins = (int) $hm[1];
return $hours + $mins;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Services\Importers;
use App\Models\Acars;
use App\Models\Aircraft;
use App\Models\Airline;
use App\Models\Airport;
use App\Models\Bid;
use App\Models\Expense;
use App\Models\File;
use App\Models\Flight;
use App\Models\FlightField;
use App\Models\FlightFieldValue;
use App\Models\Journal;
use App\Models\JournalTransaction;
use App\Models\Ledger;
use App\Models\News;
use App\Models\Pirep;
use App\Models\Subfleet;
use App\Models\User;
use App\Models\UserAward;
use Illuminate\Support\Facades\DB;
class ClearDatabase extends BaseImporter
{
/**
* Returns a default manifest just so this step gets run
*/
public function getManifest(): array
{
return [
[
'importer' => static::class,
'start' => 0,
'end' => 1,
'message' => 'Clearing database',
],
];
}
public function run($start = 0)
{
$this->cleanupDb();
}
/**
* Cleanup the local database of any users and other data that might conflict
* before running the importer
*/
protected function cleanupDb()
{
$this->info('Running database cleanup/empty before starting');
DB::statement('SET FOREIGN_KEY_CHECKS=0');
Bid::truncate();
File::truncate();
News::truncate();
Expense::truncate();
JournalTransaction::truncate();
Journal::truncate();
Ledger::truncate();
// Clear flights
DB::table('flight_fare')->truncate();
DB::table('flight_subfleet')->truncate();
FlightField::truncate();
FlightFieldValue::truncate();
Flight::truncate();
Subfleet::truncate();
Aircraft::truncate();
Airline::truncate();
Airport::truncate();
Acars::truncate();
Pirep::truncate();
UserAward::truncate();
User::truncate();
// Clear permissions
DB::table('permission_role')->truncate();
DB::table('permission_user')->truncate();
DB::table('role_user')->truncate();
// Role::truncate();
DB::statement('SET FOREIGN_KEY_CHECKS=1');
$this->idMapper->clear();
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Services\Importers;
use App\Models\Enums\ExpenseType;
use App\Models\Expense;
class ExpenseImporter extends BaseImporter
{
protected $table = 'expenses';
private $expense_types = [
'M' => ExpenseType::MONTHLY,
'F' => ExpenseType::FLIGHT,
'P' => ExpenseType::MONTHLY, // percent, monthly
'G' => ExpenseType::FLIGHT, // percent, per-flight
];
/**
* {@inheritdoc}
*/
public function run($start = 0)
{
$this->comment('--- EXPENSES IMPORT ---');
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start);
foreach ($rows as $row) {
$attrs = [
'airline_id' => null,
'name' => $row->name,
'amount' => $row->amount,
'type' => $this->expense_types[$row->type],
'active' => 1,
'ref_model' => Expense::class,
];
$expense = Expense::updateOrCreate(['name' => $row->name], $attrs);
$this->idMapper->addMapping('expenses', $row->id, $expense->id);
$this->idMapper->addMapping('expenses', $row->name, $expense->id);
$count++;
}
$this->info('Imported '.$count.' expenses');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Services\Importers;
use App\Models\Airline;
use App\Models\Expense;
use App\Services\FinanceService;
use App\Support\Money;
use Prettus\Validator\Exceptions\ValidatorException;
class ExpenseLogImporter extends BaseImporter
{
protected $table = 'expenselog';
/**
* {@inheritdoc}
*
* @throws ValidatorException
*/
public function run($start = 0)
{
$this->comment('--- EXPENSE LOG IMPORT ---');
/** @var FinanceService $financeSvc */
$financeSvc = app(FinanceService::class);
$airline = Airline::first();
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start);
foreach ($rows as $row) {
$expense_id = $this->idMapper->getMapping('expenses', $row->name);
$expense = Expense::find($expense_id);
$debit = Money::createFromAmount($expense->amount);
$financeSvc->debitFromJournal(
$airline->journal,
$debit,
$airline,
'Expense: '.$expense->name,
$expense->transaction_group ?? 'Expenses',
'expense'
);
$count++;
}
$this->info('Imported '.$count.' expense logs');
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Services\Importers;
use App\Models\User;
use App\Services\AircraftService;
use App\Services\UserService;
class FinalizeImporter extends BaseImporter
{
/**
* Returns a default manifest just so this step gets run
*/
public function getManifest(): array
{
return [
[
'importer' => static::class,
'start' => 0,
'end' => 1,
'message' => 'Finalizing import',
],
];
}
/**
* The start method. Takes the offset to start from
*
* @param int $start
*
* @return mixed
*/
public function run($start = 0)
{
$this->findLastPireps();
$this->recalculateUserStats();
$this->clearValueStore();
}
/**
* Go through and set the last PIREP ID for the users
*/
protected function findLastPireps()
{
// TODO
}
/**
* Recalculate all of the user stats
*/
protected function recalculateUserStats()
{
$this->comment('--- RECALCULATING USER STATS ---');
/** @var UserService $userSvc */
$userSvc = app(UserService::class);
User::all()->each(function ($user) use ($userSvc) {
$userSvc->recalculateStats($user);
});
}
/**
* Update the aircraft stats with the newest/latest PIREPs
*/
protected function recalculateAircraftStats()
{
$this->comment('--- RECALCULATING AIRCRAFT STATS ---');
/** @var AircraftService $aircraftSvc */
$aircraftSvc = app(AircraftService::class);
$aircraftSvc->recalculateStats();
}
/**
* Clear the value store of any old value mappings
*/
protected function clearValueStore()
{
$this->idMapper->clear();
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Services\Importers;
use App\Models\Flight;
class FlightImporter extends BaseImporter
{
protected $table = 'schedules';
public function run($start = 0)
{
$this->comment('--- FLIGHT SCHEDULE IMPORT ---');
$fields = [
'id',
'code',
'flightnum',
'depicao',
'arricao',
'route',
'distance',
'flightlevel',
'deptime',
'arrtime',
'flighttime',
'notes',
'enabled',
];
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start, $fields);
foreach ($rows as $row) {
$airline_id = $this->idMapper->getMapping('airlines', $row->code);
$flight_num = trim($row->flightnum);
$attrs = [
'dpt_airport_id' => $row->depicao,
'arr_airport_id' => $row->arricao,
'route' => $row->route ?: '',
'distance' => round($row->distance ?: 0, 2),
'level' => $row->flightlevel ?: 0,
'dpt_time' => $row->deptime ?: '',
'arr_time' => $row->arrtime ?: '',
'flight_time' => $this->convertDuration($row->flighttime) ?: '',
'notes' => $row->notes ?: '',
'active' => $row->enabled ?: true,
];
try {
$w = ['airline_id' => $airline_id, 'flight_number' => $flight_num];
// $flight = Flight::updateOrCreate($w, $attrs);
$flight = Flight::create(array_merge($w, $attrs));
} catch (\Exception $e) {
$this->error($e);
}
$this->idMapper->addMapping('flights', $row->id, $flight->id);
// TODO: deserialize route_details into ACARS table
if ($flight->wasRecentlyCreated) {
$count++;
}
}
$this->info('Imported '.$count.' flights');
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Services\Importers;
use App\Models\Permission;
use App\Models\Role;
use App\Services\RoleService;
use Illuminate\Support\Facades\Log;
/**
* Imports the groups into the permissions feature(s)
*/
class GroupImporter extends BaseImporter
{
protected $table = 'groups';
protected $idField = 'groupid';
/**
* Permissions in the legacy system, mapping them to the current system
*/
protected $legacy_permission_set = [
'EDIT_NEWS' => 0x1,
'EDIT_PAGES' => 0x2,
'EDIT_DOWNLOADS' => 0x4,
'EMAIL_PILOTS' => 0x8,
'EDIT_AIRLINES' => 0x10, //
'EDIT_FLEET' => 0x20, //
'EDIT_SCHEDULES' => 0x80, //
'IMPORT_SCHEDULES' => 0x100, //
'MODERATE_REGISTRATIONS' => 0x200,
'EDIT_PILOTS' => 0x400, //
'EDIT_GROUPS' => 0x800,
'EDIT_RANKS' => 0x1000, //
'EDIT_AWARDS' => 0x2000, //
'MODERATE_PIREPS' => 0x4000, //
'VIEW_FINANCES' => 0x8000, //
'EDIT_EXPENSES' => 0x10000, //
'EDIT_SETTINGS' => 0x20000, //
'EDIT_PIREPS_FIELDS' => 0x40000, //
'EDIT_PROFILE_FIELDS' => 0x80000, //
'EDIT_VACENTRAL' => 0x100000,
'ACCESS_ADMIN' => 0x2000000,
'FULL_ADMIN' => 35651519, //
];
/**
* Map a legacy value over to one of the current permission values
*/
protected $legacy_to_permission = [
'FULL_ADMIN' => 'admin',
'EDIT_AIRLINES' => 'airlines',
'EDIT_AWARDS' => 'awards',
'EDIT_FLEET' => 'fleet',
'EDIT_EXPENCES' => 'finances',
'VIEW_FINANCES' => 'finances',
'EDIT_SCHEDULES' => 'flights',
'EDIT_PILOTS' => 'users',
'EDIT_PROFILE_FIELDS' => 'users',
'EDIT_SETTINGS' => 'settings',
'MODERATE_PIREPS' => 'pireps',
'EDIT_PIREPS_FIELDS' => 'pireps',
'EDIT_RANKS' => 'ranks',
'MODERATE_REGISTRATIONS' => 'users',
];
public function run($start = 0)
{
$this->comment('--- ROLES/GROUPS IMPORT ---');
/** @var \App\Services\RoleService $roleSvc */
$roleSvc = app(RoleService::class);
$permMappings = $this->getPermissions();
$count = 0;
$permCount = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start);
foreach ($rows as $row) {
// Legacy "administrator" role is now "admin", just map that 1:1
if (strtolower($row->name) === 'administrators') {
$role = Role::where('name', 'admin')->first();
$this->idMapper->addMapping('group', $row->groupid, $role->id);
continue;
}
// Map the "core" roles, which are active/inactive pilots to a new ID of
// -1; so then we can ignore/not add these groups, and then ignore them
// for any of the users that are being imported. these groups are unused
if ($row->core === 1 || $row->core === '1') {
$this->idMapper->addMapping('group', $row->groupid, -1);
continue;
}
$name = str_slug($row->name);
$role = Role::firstOrCreate(
['name' => $name],
['display_name' => $row->name]
);
$this->idMapper->addMapping('group', $row->groupid, $role->id);
// See if the permission set mask contains one of the mappings above
// Add all of the ones which apply, and then set them on the new role
$permissions = [];
foreach ($this->legacy_permission_set as $legacy_name => $mask) {
$val = $row->permissions & $mask;
if ($val === $mask) {
// Map this legacy permission to what it is under the new system
if (!array_key_exists($legacy_name, $this->legacy_to_permission)) {
continue;
}
// Get the ID of the permission
try {
$permName = $this->legacy_to_permission[$legacy_name];
if ($permName === 'admin') {
foreach ($permMappings as $name => $value) {
if (!in_array($value, $permissions, true)) {
$permissions[] = $value;
}
}
continue;
}
$permMapId = $permMappings[$permName];
if (!in_array($permMapId, $permissions, true)) {
$permissions[] = $permMapId;
}
} catch (\Exception $e) {
Log::error($e->getMessage());
}
}
}
if (count($permissions) > 0) {
$roleSvc->setPermissionsForRole($role, $permissions);
$permCount += count($permissions);
}
if ($role->wasRecentlyCreated) {
$count++;
}
}
$this->info('Imported '.$count.' roles, synced '.$permCount.' permissions');
}
/**
* Get all of the permissions from locally and return a kvp with the
* key being the permission short-name and the value being the ID
*
* @return array
*/
private function getPermissions(): array
{
$mappings = [];
$permissions = Permission::all();
/** @var \App\Models\Permission $p */
foreach ($permissions as $p) {
$mappings[$p->name] = $p->id;
}
return $mappings;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Services\Importers;
use App\Models\Pirep;
use App\Services\FinanceService;
use App\Support\Money;
class LedgerImporter extends BaseImporter
{
protected $table = 'ledger';
/**
* Legacy ID to the current ledger ref_model mapping
*
* @var array
*/
private static $legacy_paysource = [
1 => Pirep::class,
];
/**
* {@inheritdoc}
*
* @throws \Prettus\Validator\Exceptions\ValidatorException
*/
public function run($start = 0)
{
if (!$this->db->tableExists('ledger')) {
return;
}
$this->comment('--- LEDGER IMPORT ---');
/** @var FinanceService $financeSvc */
$financeSvc = app(FinanceService::class);
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start);
foreach ($rows as $row) {
$pirep = Pirep::find($this->idMapper->getMapping('pireps', $row->pirepid));
if (!$pirep) {
continue;
}
$pilot_pay = Money::createFromAmount($row->amount * 100);
$memo = 'Pilot payment';
$financeSvc->debitFromJournal(
$pirep->airline->journal,
$pilot_pay,
$pirep,
$memo,
'Pilot Pay',
'pilot_pay',
$row->submitdate
);
$financeSvc->creditToJournal(
$pirep->user->journal,
$pilot_pay,
$pirep,
$memo,
'Pilot Pay',
'pilot_pay',
$row->submitdate
);
$count++;
}
$this->info('Imported '.$count.' ledger entries');
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace App\Services\Importers;
use App\Models\Enums\FlightType;
use App\Models\Enums\PirepSource;
use App\Models\Enums\PirepState;
use App\Models\Pirep;
class PirepImporter extends BaseImporter
{
protected $table = 'pireps';
protected $idField = 'pirepid';
public function run($start = 0)
{
$this->comment('--- PIREP IMPORT ---');
$fields = [
'pirepid',
'pilotid',
'code',
'aircraft',
'flightnum',
'depicao',
'arricao',
'fuelused',
'route',
'source',
'accepted',
'submitdate',
'distance',
'flighttime_stamp',
'flighttype',
];
// See if there's a flightlevel column, export that if there is
$columns = $this->db->getColumns($this->table);
if (in_array('flightlevel', $columns, true)) {
$fields[] = 'flightlevel';
}
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start, $fields);
foreach ($rows as $row) {
$pirep_id = $row->pirepid;
$user_id = $this->idMapper->getMapping('users', $row->pilotid);
$airline_id = $this->idMapper->getMapping('airlines', $row->code);
$aircraft_id = $this->idMapper->getMapping('aircraft', $row->aircraft);
$attrs = [
'user_id' => $user_id,
'airline_id' => $airline_id,
'aircraft_id' => $aircraft_id,
'flight_number' => $row->flightnum ?: '',
'dpt_airport_id' => $row->depicao,
'arr_airport_id' => $row->arricao,
'block_fuel' => $row->fuelused,
'route' => $row->route ?: '',
'source_name' => $row->source,
'state' => $this->mapState($row->accepted),
'created_at' => $this->parseDate($row->submitdate),
'updated_at' => $this->parseDate($row->submitdate),
];
// Set the distance
$distance = round($row->distance ?: 0, 2);
$attrs['distance'] = $distance;
$attrs['planned_distance'] = $distance;
// Set the flight time properly
$duration = $this->convertDuration($row->flighttime_stamp);
$attrs['flight_time'] = $duration;
$attrs['planned_flight_time'] = $duration;
// Set how it was filed
if (strtoupper($row->source) === 'MANUAL') {
$attrs['source'] = PirepSource::MANUAL;
} else {
$attrs['source'] = PirepSource::ACARS;
}
// Set the flight type
$row->flighttype = strtoupper($row->flighttype);
if ($row->flighttype === 'P') {
$attrs['flight_type'] = FlightType::SCHED_PAX;
} elseif ($row->flighttype === 'C') {
$attrs['flight_type'] = FlightType::SCHED_CARGO;
} else {
$attrs['flight_type'] = FlightType::CHARTER_PAX_ONLY;
}
// Set the flight level of the PIREP is set
if (property_exists($row, 'flightlevel')) {
$attrs['level'] = $row->flightlevel;
} else {
$attrs['level'] = 0;
}
$w = ['id' => $pirep_id];
$pirep = Pirep::updateOrCreate($w, $attrs);
//$pirep = Pirep::create(array_merge($w, $attrs));
//Log::debug('pirep oldid='.$pirep_id.', olduserid='.$row->pilotid
// .'; new id='.$pirep->id.', user id='.$user_id);
$source = strtoupper($row->source);
if ($source === 'SMARTCARS') {
// TODO: Parse smartcars log into the acars table
} elseif ($source === 'KACARS') {
// TODO: Parse kACARS log into acars table
} elseif ($source === 'XACARS') {
// TODO: Parse XACARS log into acars table
}
// TODO: Add extra fields in as PIREP fields
$this->idMapper->addMapping('pireps', $row->pirepid, $pirep->id);
if ($pirep->wasRecentlyCreated) {
$count++;
}
}
$this->info('Imported '.$count.' pireps');
}
/**
* Map the old status to the current
* https://github.com/nabeelio/phpvms_v2/blob/master/core/app.config.php#L450
*
* @param int $old_state
*
* @return int
*/
private function mapState($old_state)
{
$map = [
0 => PirepState::PENDING,
1 => PirepState::ACCEPTED,
2 => PirepState::REJECTED,
3 => PirepState::IN_PROGRESS,
];
$old_state = (int) $old_state;
if (!in_array($old_state, $map, true)) {
return PirepState::PENDING;
}
return $map[$old_state];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Services\Importers;
use App\Models\Rank;
class RankImport extends BaseImporter
{
protected $table = 'ranks';
protected $idField = 'rankid';
public function run($start = 0)
{
$this->comment('--- RANK IMPORT ---');
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start);
foreach ($rows as $row) {
$rank = Rank::updateOrCreate(['name' => $row->rank], [
'image_url' => $row->rankimage,
'hours' => $row->minhours,
'acars_base_payrate' => $row->payrate,
'manual_base_payrate' => $row->payrate,
]);
$this->idMapper->addMapping('ranks', $row->rankid, $rank->id);
$this->idMapper->addMapping('ranks', $row->rank, $rank->id);
if ($rank->wasRecentlyCreated) {
$count++;
}
}
$this->info('Imported '.$count.' ranks');
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Services\Importers;
use App\Repositories\SettingRepository;
class SettingsImporter extends BaseImporter
{
protected $table = 'settings';
public function run($start = 0)
{
$this->comment('--- SETTINGS IMPORT ---');
/** @var SettingRepository $settingsRepo */
$settingsRepo = app(SettingRepository::class);
$count = 0;
$rows = $this->db->readRows($this->table, $this->idField, $start);
foreach ($rows as $row) {
switch ($row->name) {
case 'ADMIN_EMAIL':
$settingsRepo->store('general.admin_email', $row->value);
break;
}
}
$this->info('Imported '.$count.' settings');
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace App\Services\Importers;
use App\Models\Enums\UserState;
use App\Models\User;
use App\Services\UserService;
use App\Support\Units\Time;
use App\Support\Utils;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class UserImport extends BaseImporter
{
protected $table = 'pilots';
protected $idField = 'pilotid';
/**
* @var UserService
*/
private $userSvc;
public function run($start = 0)
{
$this->comment('--- USER IMPORT ---');
$this->userSvc = app(UserService::class);
$count = 0;
$first_row = true;
$rows = $this->db->readRows($this->table, $this->idField, $start);
foreach ($rows as $row) {
$pilot_id = $row->pilotid; // This isn't their actual ID
$name = $row->firstname.' '.$row->lastname;
// Figure out which airline, etc, they belong to
$airline_id = $this->idMapper->getMapping('airlines', $row->code);
// Log::info('User airline from '.$row->code.' to ID '.$airline_id);
$rank_id = $this->idMapper->getMapping('ranks', $row->rank);
$state = $this->getUserState($row->retired);
if ($first_row) {
$new_password = 'admin';
$first_row = false;
} else {
$new_password = Str::random(60);
}
// Look for a user with that pilot ID already. If someone has it
if ($this->userSvc->isPilotIdAlreadyUsed($pilot_id)) {
Log::info('User with pilot id '.$pilot_id.' exists');
// Is this the same user? If not, get a new pilot ID
$user_exist = User::where('pilot_id', $pilot_id)->first();
if ($user_exist->email !== $row->email) {
$pilot_id = $this->userSvc->getNextAvailablePilotId();
}
}
$attrs = [
'pilot_id' => $pilot_id,
'name' => $name,
'password' => Hash::make($new_password),
'api_key' => Utils::generateApiKey(),
'airline_id' => $airline_id,
'rank_id' => $rank_id,
'home_airport_id' => $row->hub,
'curr_airport_id' => $row->hub,
'country' => $row->location,
'flights' => (int) $row->totalflights,
'flight_time' => Time::hoursToMinutes($row->totalhours),
'state' => $state,
'created_at' => $this->parseDate($row->joindate),
];
$user = User::updateOrCreate(['email' => $row->email], $attrs);
$this->idMapper->addMapping('users', $row->pilotid, $user->id);
$this->updateUserRoles($user, $row->pilotid);
if ($user->wasRecentlyCreated) {
$count++;
}
}
$this->info('Imported '.$count.' users');
}
/**
* Update a user's roles and add them to the proper ones
*
* @param User $user
* @param string $old_pilot_id
*/
protected function updateUserRoles(User $user, $old_pilot_id)
{
// Be default add them to the user role, and then determine if they
// belong to any other groups, and add them to that
$newRoles = [];
// Figure out what other groups they belong to... read from the old table, and map
// them to the new group(s)
$old_user_groups = $this->db->findBy('groupmembers', ['pilotid' => $old_pilot_id]);
foreach ($old_user_groups as $oldGroup) {
$newRoleId = $this->idMapper->getMapping('group', $oldGroup->groupid);
// This role should be ignored
if ($newRoleId === -1) {
continue;
}
$newRoles[] = $newRoleId;
}
// Assign the groups to the new user
$user->attachRoles($newRoles);
}
/**
* Get the user's new state from their original state
*
* @param $state
*
* @return int
*/
protected function getUserState($state)
{
// Return active for now, let the stats/cron determine the status later
return UserState::ACTIVE;
/*$state = (int) $state;
// Declare array of classic states
$phpvms_classic_states = [
'ACTIVE' => 0,
'INACTIVE' => 1,
'BANNED' => 2,
'ON_LEAVE' => 3,
];
// Decide which state they will be in accordance with v7
if ($state === $phpvms_classic_states['ACTIVE']) {
return UserState::ACTIVE;
}
if ($state === $phpvms_classic_states['INACTIVE']) {
// TODO: Make an inactive state?
return UserState::REJECTED;
}
if ($state === $phpvms_classic_states['BANNED']) {
return UserState::SUSPENDED;
}
if ($state === $phpvms_classic_states['ON_LEAVE']) {
return UserState::ON_LEAVE;
}
$this->error('Unknown status: '.$state);
return UserState::ACTIVE;*/
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace App\Services\Installer;
use App\Contracts\Service;
use Exception;
use function extension_loaded;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use function is_bool;
use Nwidart\Modules\Support\Stub;
use PDO;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
class ConfigService extends Service
{
/**
* Create the .env file
*
* @param $attrs
*
* @throws \Symfony\Component\HttpFoundation\File\Exception\FileException
*
* @return bool
*/
public function createConfigFiles($attrs): bool
{
$opts = [
'APP_ENV' => 'dev',
'APP_KEY' => $this->createAppKey(),
'SITE_NAME' => '',
'SITE_URL' => 'http://phpvms.test',
'CACHE_PREFIX' => '',
'DB_CONN' => '',
'DB_HOST' => '',
'DB_PORT' => 3306,
'DB_NAME' => '',
'DB_USER' => '',
'DB_PASS' => '',
'DB_PREFIX' => '',
'DB_EMULATE_PREPARES' => false,
];
$opts = array_merge($opts, $attrs);
$opts = $this->determinePdoOptions($opts);
$opts = $this->configCacheDriver($opts);
$opts = $this->configQueueDriver($opts);
$this->writeConfigFiles($opts);
return true;
}
/**
* Update the environment file and update certain keys/values
*
* @param array $kvp
*
* @return void
*/
public function updateKeysInEnv(array $kvp)
{
$app = app();
$env_file = file_get_contents($app->environmentFilePath());
foreach ($kvp as $key => $value) {
$key = strtoupper($key);
// cast for any boolean values
if (is_bool($value)) {
$value = $value === true ? 'true' : 'false';
}
// surround by quotes if there are any spaces in the value
if (strpos($value, ' ') !== false) {
$value = '"'.$value.'"';
}
Log::info('Replacing "'.$key.'" with '.$value);
$env_file = preg_replace(
'/^'.$key.'(.*)?/m',
$key.'='.$value,
$env_file
);
}
file_put_contents($app->environmentFilePath(), $env_file);
}
/**
* Generate a fresh new APP_KEY
*
* @return string
*/
protected function createAppKey(): string
{
return base64_encode(Encrypter::generateKey(config('app.cipher')));
}
/**
* Change a few options within the PDO driver, depending on the version
* of mysql/maria, etc used. ATM, only make a change for MariaDB
*
* @param $opts
*
* @return mixed
*/
protected function determinePdoOptions($opts)
{
if ($opts['DB_CONN'] !== 'mysql') {
return $opts;
}
$dsn = "mysql:host=$opts[DB_HOST];port=$opts[DB_PORT];";
Log::info('Connection string: '.$dsn);
$conn = new PDO($dsn, $opts['DB_USER'], $opts['DB_PASS']);
$version = strtolower($conn->getAttribute(PDO::ATTR_SERVER_VERSION));
Log::info('Detected DB Version: '.$version);
// If it's mariadb, enable the emulation for prepared statements
// seems to be throwing a problem on 000webhost
// https://github.com/nabeelio/phpvms/issues/132
if (strpos($version, 'mariadb') !== false) {
Log::info('Detected MariaDB, setting DB_EMULATE_PREPARES to true');
$opts['DB_EMULATE_PREPARES'] = true;
}
return $opts;
}
/**
* Determine is APC is installed, if so, then use it as a cache driver
*
* @param $opts
*
* @return mixed
*/
protected function configCacheDriver($opts)
{
// Set the cache prefix
$opts['CACHE_PREFIX'] = uniqid($opts['SITE_NAME'].'_');
// Figure out what cache driver to initially use, depending on
// what is installed. It won't detect redis or anything, though
foreach (config('installer.cache.drivers') as $ext => $driver) {
if (extension_loaded($ext)) {
Log::info('Detected extension "'.$ext.'", setting driver to "'.$driver.'"');
$opts['CACHE_DRIVER'] = $driver;
return $opts;
}
}
Log::info('No extension detected, using file cache');
$opts['CACHE_DRIVER'] = config('installer.cache.default');
return $opts;
}
/**
* Setup a queue driver that's not the default "sync"
* driver, if a database is being used
*
* @param $opts
*
* @return mixed
*/
protected function configQueueDriver($opts)
{
// If we're setting up a database, then also setup
// the default queue driver to use the database
if ($opts['DB_CONN'] === 'mysql' || $opts['DB_CONN'] === 'postgres') {
$opts['QUEUE_DRIVER'] = 'database';
} else {
$opts['QUEUE_DRIVER'] = 'sync';
}
return $opts;
}
/**
* Remove the config files
*/
public function removeConfigFiles()
{
$env_file = App::environmentFilePath();
$config_file = App::environmentPath().'/config.php';
if (file_exists($env_file)) {
try {
unlink($env_file);
} catch (Exception $e) {
Log::error($e->getMessage());
}
}
if (file_exists($config_file)) {
try {
unlink($config_file);
} catch (Exception $e) {
Log::error($e->getMessage());
}
}
}
/**
* Get the template file name and write it out
*
* @param $opts
*
* @throws \Symfony\Component\HttpFoundation\File\Exception\FileException
*/
protected function writeConfigFiles($opts)
{
Stub::setBasePath(resource_path('/stubs/installer'));
$env_file = App::environmentFilePath();
if (file_exists($env_file) && !is_writable($env_file)) {
Log::error('Permissions on existing env.php is not writable');
throw new FileException('Can\'t write to the env.php file! Check the permissions');
}
/*
* First write out the env file
*/
try {
$stub = new Stub('/env.stub', $opts);
$stub->render();
$stub->saveTo(App::environmentPath(), App::environmentFile());
} catch (Exception $e) {
throw new FileException('Couldn\'t write env.php. ('.$e.')');
}
/*
* Next write out the config file. If there's an error here,
* then throw an exception but delete the env file first
*/
try {
$stub = new Stub('/config.stub', $opts);
$stub->render();
$stub->saveTo(App::environmentPath(), 'config.php');
} catch (Exception $e) {
unlink(App::environmentPath().'/'.App::environmentFile());
throw new FileException('Couldn\'t write config.php. ('.$e.')');
}
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Services\Installer;
use App\Contracts\Service;
use Illuminate\Support\Facades\Artisan;
use Nwidart\Modules\Facades\Module;
class InstallerService extends Service
{
@@ -44,27 +43,6 @@ class InstallerService extends Service
*/
public function clearCaches(): void
{
$commands = [
'clear-compiled',
'cache:clear',
'route:clear',
'view:clear',
];
foreach ($commands as $cmd) {
Artisan::call($cmd);
}
}
/**
* Disable the installer and importer modules
*/
public function disableInstallerModules()
{
$module = Module::find('installer');
$module->disable();
$module = Module::find('importer');
$module->disable();
Artisan::call('optimize:clear');
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Services\Installer;
use App\Contracts\Service;
class RequirementsService extends Service
{
/**
* Check the PHP version that it meets the minimum requirement
*
* @return array
*/
public function checkPHPVersion(): array
{
$passed = false;
if (version_compare(PHP_VERSION, config('installer.php.version')) >= 0) {
$passed = true;
}
return ['version' => PHP_VERSION, 'passed' => $passed];
}
/**
* Make sure the minimal extensions required are loaded
*
* @return array
*/
public function checkExtensions(): array
{
$extensions = [];
foreach (config('installer.extensions') as $ext) {
$pass = true;
if (!\extension_loaded($ext)) {
$pass = false;
}
$extensions[] = [
'ext' => $ext,
'passed' => $pass,
];
}
return $extensions;
}
/**
* Check the permissions for the directories specified
* Make sure they exist and are writable
*
* @return array
*/
public function checkPermissions(): array
{
clearstatcache();
$directories = [];
foreach (config('installer.permissions') as $path) {
$pass = true;
if (!file_exists($path)) {
$pass = false;
}
if (!is_writable($path)) {
$pass = false;
}
$directories[] = [
'dir' => $path,
'passed' => $pass,
];
}
return $directories;
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Services\Installer;
use App\Contracts\Service;
use App\Models\Setting;
use App\Services\DatabaseService;
use Carbon\Carbon;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -42,6 +43,10 @@ class SeederService extends Service
return true;
}
if ($this->moduleSeedsPending()) {
return true;
}
return false;
}
@@ -54,6 +59,7 @@ class SeederService extends Service
$this->syncAllYamlFileSeeds();
$this->syncAllSettings();
$this->syncAllPermissions();
$this->syncAllModules();
}
/**
@@ -83,6 +89,24 @@ class SeederService extends Service
});
}
public function syncAllModules(): void
{
$data = file_get_contents(database_path('/seeds/modules.yml'));
$yml = Yaml::parse($data);
foreach ($yml as $module) {
$module['updated_at'] = Carbon::now();
$count = DB::table('modules')->where('name', $module['name'])->count('name');
if ($count === 0) {
$module['created_at'] = Carbon::now();
DB::table('modules')->insert($module);
} else {
DB::table('modules')
->where('name', $module['name'])
->update($module);
}
}
}
public function syncAllSettings(): void
{
$data = file_get_contents(database_path('/seeds/settings.yml'));
@@ -286,4 +310,28 @@ class SeederService extends Service
return false;
}
private function moduleSeedsPending(): bool
{
$all_modules = DB::table('modules')->get();
$data = file_get_contents(database_path('/seeds/modules.yml'));
$yml = Yaml::parse($data);
foreach ($yml as $perm) {
$row = $all_modules->firstWhere('name', $perm['name']);
if (!$row) {
return true;
}
// See if any of these column values have changed
foreach (['name', 'enabled'] as $column) {
if ($row->{$column} !== $perm[$column]) {
return true;
}
}
}
return false;
}
}

View File

@@ -3,6 +3,22 @@
namespace App\Services;
use App\Contracts\Service;
use App\Exceptions\ModuleExistsException;
use App\Exceptions\ModuleInstallationError;
use App\Exceptions\ModuleInvalidFileType;
use App\Models\Module;
use Exception;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Laracasts\Flash\FlashNotifier;
use Madnest\Madzipper\Madzipper;
use Nwidart\Modules\Json;
use PharData;
class ModuleService extends Service
{
@@ -22,14 +38,14 @@ class ModuleService extends Service
* @param string $title
* @param string $url
* @param string $icon
* @param mixed $logged_in
* @param bool $logged_in
*/
public function addFrontendLink(string $title, string $url, string $icon = '', $logged_in = true)
public function addFrontendLink(string $title, string $url, string $icon = 'pe-7s-users', $logged_in = true)
{
self::$frontendLinks[$logged_in][] = [
'title' => $title,
'url' => $url,
'icon' => 'pe-7s-users',
'icon' => $icon,
];
}
@@ -52,12 +68,12 @@ class ModuleService extends Service
* @param string $url
* @param string $icon
*/
public function addAdminLink(string $title, string $url, string $icon = '')
public function addAdminLink(string $title, string $url, string $icon = 'pe-7s-users')
{
self::$adminLinks[] = [
'title' => $title,
'url' => $url,
'icon' => 'pe-7s-users',
'icon' => $icon,
];
}
@@ -70,4 +86,217 @@ class ModuleService extends Service
{
return self::$adminLinks;
}
/**
* Get All modules from Database
*
* @return object
*/
public function getAllModules(): object
{
return Module::all();
}
/**
* Get Module Information from Database.
*
* @param $id
*
* @return object
*/
public function getModule($id): object
{
return Module::find($id);
}
/**
* Adding installed module to the database
*
* @param $module_name
*
* @return bool
*/
public function addModule($module_name): bool
{
/*Check if module already exists*/
$module = Module::where('name', $module_name);
if (!$module->exists()) {
Module::create([
'name' => $module_name,
'enabled' => 1,
]);
return true;
}
return false;
}
/**
* User's uploaded file is passed into this method
* to install module in the Storage.
*
* @param UploadedFile $file
*
* @return FlashNotifier
*/
public function installModule(UploadedFile $file): FlashNotifier
{
$file_ext = $file->getClientOriginalExtension();
$allowed_extensions = ['zip', 'tar', 'gz'];
if (!in_array($file_ext, $allowed_extensions, true)) {
throw new ModuleInvalidFileType();
}
$module = null;
$new_dir = rand();
File::makeDirectory(
storage_path('app/tmp/modules/'.$new_dir),
0777,
true
);
$temp_ext_folder = storage_path('app/tmp/modules/'.$new_dir);
$temp = storage_path('app/tmp/modules/'.$new_dir);
$zipper = null;
if ($file_ext === 'tar' || $file_ext === 'gz') {
$zipper = new PharData($file);
$zipper->decompress();
}
if ($file_ext === 'zip') {
$madZipper = new Madzipper();
try {
$zipper = $madZipper->make($file);
} catch (Exception $e) {
throw new ModuleInstallationError();
}
}
try {
$zipper->extractTo($temp);
} catch (Exception $e) {
throw new ModuleInstallationError();
}
if (!File::exists($temp.'/module.json')) {
$directories = Storage::directories('tmp/modules/'.$new_dir);
$temp = storage_path('app/'.$directories[0]);
}
$json_file = $temp.'/module.json';
if (File::exists($json_file)) {
$json = json_decode(file_get_contents($json_file), true);
$module = $json['name'];
} else {
File::deleteDirectory($temp_ext_folder);
return flash()->error('Module Structure Not Correct!');
}
if (!$module) {
File::deleteDirectory($temp_ext_folder);
return flash()->error('Not a Valid Module File.');
}
$toCopy = base_path().'/modules/'.$module;
if (File::exists($toCopy)) {
File::deleteDirectory($temp_ext_folder);
throw new ModuleExistsException($module);
}
File::moveDirectory($temp, $toCopy);
File::deleteDirectory($temp_ext_folder);
try {
$this->addModule($module);
} catch (Exception $e) {
throw new ModuleExistsException($module);
}
Artisan::call('config:cache');
Artisan::call('module:migrate '.$module);
return flash()->success('Module Installed');
}
/**
* Update module with the status passed by user.
*
* @param $id
* @param $status
*
* @return bool
*/
public function updateModule($id, $status): bool
{
$module = Module::find($id);
$module->update([
'enabled' => $status,
]);
return true;
}
/**
* Delete Module from the Storage & Database.
*
* @param $id
* @param $data
*
* @return bool
*/
public function deleteModule($id, $data): bool
{
$module = Module::find($id);
if ($data['verify'] === strtoupper($module->name)) {
try {
$module->delete();
} catch (Exception $e) {
Log::emergency('Cannot Delete Module!');
}
$moduleDir = base_path().'/modules/'.$module->name;
try {
File::deleteDirectory($moduleDir);
} catch (Exception $e) {
Log::info('Folder Deleted Manually for Module : '.$module->name);
return true;
}
return true;
}
return false;
}
/**
* Get & scan all modules.
*
* @return array
*/
public function scan()
{
$modules_path = base_path('modules/*');
$path = Str::endsWith($modules_path, '/*') ? $modules_path : Str::finish($modules_path, '/*');
$modules = [];
$manifests = (new Filesystem())->glob("{$path}/module.json");
is_array($manifests) || $manifests = [];
foreach ($manifests as $manifest) {
$name = Json::make($manifest)->get('name');
$module = Module::where('name', $name);
if (!$module->exists()) {
array_push($modules, $name);
}
}
return $modules;
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace App\Support\Modules;
use Exception;
use Illuminate\Config\Repository as Config;
use Illuminate\Container\Container;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Log;
use Nwidart\Modules\Contracts\ActivatorInterface;
use Nwidart\Modules\Module;
class DatabaseActivator implements ActivatorInterface
{
/**
* Laravel config instance
*
* @var Config
*/
private $config;
/**
* @var Filesystem
*/
private $files;
/**
* The module path.
*
* @var string|null
*/
protected $path;
/**
* The scanned paths.
*
* @var array
*/
protected $paths = [];
/**
* Array of modules activation statuses
*
* @var array
*/
private $modulesStatuses;
public function __construct(Container $app, $path = null)
{
$this->config = $app['config'];
$this->files = $app['files'];
$this->modulesStatuses = $this->getModulesStatuses();
$this->path = $path;
}
/**
* Get modules statuses, from the database
*
* @return array
*/
private function getModulesStatuses(): array
{
try {
$modules = \App\Models\Module::all();
$retVal = [];
foreach ($modules as $i) {
$retVal[$i->name] = $i->enabled;
}
return $retVal;
} catch (Exception $e) {
return [];
}
}
/**
* {@inheritdoc}
*/
public function reset(): void
{
(new \App\Models\Module())->truncate();
}
/**
* {@inheritdoc}
*/
public function enable(Module $module): void
{
$this->setActiveByName($module->getName(), true);
}
/**
* {@inheritdoc}
*/
public function disable(Module $module): void
{
$this->setActiveByName($module->getName(), false);
}
/**
* \Nwidart\Modules\Module instance passed
* {@inheritdoc}
*/
public function hasStatus(Module $module, bool $status): bool
{
try {
$module = (new \App\Models\Module())->where('name', $module->getName());
if ($module->exists()) {
return $module->first()->enabled == 1;
}
return false;
} catch (Exception $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function setActive(Module $module, bool $active): void
{
$this->setActiveByName($module->getName(), $active);
}
/**
* {@inheritdoc}
*/
public function setActiveByName(string $name, bool $status): void
{
$module = (new \App\Models\Module())->where('name', $name);
if ($module->exists()) {
$module->update([
'status' => $status,
]);
}
}
/**
* {@inheritdoc}
*/
public function delete(Module $module): void
{
$name = $module->getName();
try {
(new \App\Models\Module())->where([
'name' => $name,
])->delete();
} catch (Exception $e) {
Log::error('Module '.$module.' Delete failed! Exception : '.$e->getMessage());
return;
}
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Support;
use App\Services\VersionService;
use Codedge\Updater\Contracts\GithubRepositoryTypeContract;
use Codedge\Updater\SourceRepositoryTypes\GithubRepositoryType;
use Codedge\Updater\Traits\SupportPrivateAccessToken;
use Codedge\Updater\Traits\UseVersionFile;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
/**
* Use SourceRepositoryTypes/GithubRepositoryTypes/GithubTagType.php as a reference
* Just replace the new update checks, etc, with the VersionService stubs. They're
* essentially the same except for the current version checks and all that. Adds some
* additional logging too, but the base update method is from GithubRepositoryType
*/
final class VmsRepositoryType extends GithubRepositoryType implements GithubRepositoryTypeContract
{
use UseVersionFile;
use SupportPrivateAccessToken;
/**
* @var Client
*/
protected $client;
/**
* @var VersionService
*/
protected $versionSvc;
/**
* @var string
*/
private $storagePath;
public function __construct(array $config, Client $client)
{
$this->config = $config;
$this->client = $client;
$this->storagePath = Str::finish($this->config['download_path'], DIRECTORY_SEPARATOR);
$this->versionSvc = app(VersionService::class);
}
/**
* Check repository if a newer version than the installed one is available.
*
* @param string $currentVersion
*
* @throws InvalidArgumentException
* @throws Exception
*
* @return bool
*/
public function isNewVersionAvailable(string $currentVersion = ''): bool
{
return $this->versionSvc->isNewVersionAvailable($currentVersion);
}
/**
* Get the latest version that has been published in a certain repository.
* Example: 2.6.5 or v2.6.5.
*
* @param string $prepend Prepend a string to the latest version
* @param string $append Append a string to the latest version
*
* @throws Exception
*
* @return string
*/
public function getVersionAvailable(string $prepend = '', string $append = ''): string
{
return $this->versionSvc->getLatestVersion();
}
/**
* Fetches the latest version. If you do not want the latest version, specify one and pass it.
*
* @param string $version
*
* @throws Exception
*
* @return void
*/
public function fetch($version = ''): void
{
$response = $this->getRepositoryReleases();
$releaseCollection = collect(\GuzzleHttp\json_decode($response->getBody()->getContents()));
if ($releaseCollection->isEmpty()) {
throw new Exception('Cannot find a release to update. Please check the repository you\'re pulling from');
}
if (!File::exists($this->storagePath)) {
File::makeDirectory($this->storagePath, 493, true, true);
}
if (!empty($version)) {
$release = $releaseCollection->where('name', $version)->first();
} else {
$release = $releaseCollection->first();
}
Log::info('Found release='.$release->name.', path='.$release->zipball_url);
$storageFolder = $this->storagePath.$release->name.'-'.now()->timestamp;
$storageFilename = $storageFolder.'.zip';
if (!$this->isSourceAlreadyFetched($release->name)) {
$this->downloadRelease($this->client, $release->zipball_url, $storageFilename);
$this->unzipArchive($storageFilename, $storageFolder);
$this->createReleaseFolder($storageFolder, $release->name);
}
}
protected function getRepositoryReleases(): ResponseInterface
{
$url = self::GITHUB_API_URL
.'/repos/'.$this->config['repository_vendor']
.'/'.$this->config['repository_name']
.'/tags';
$headers = [];
return $this->client->request('GET', $url, ['headers' => $headers]);
}
}

57
app/Utils/IdMapper.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace App\Utils;
use App\Contracts\Service;
use Spatie\Valuestore\Valuestore;
class IdMapper extends Service
{
private $valueStore;
public function __construct()
{
$this->valueStore = Valuestore::make(storage_path('app/legacy_migration.json'));
}
/**
* Create a new mapping between an old ID and the new one
*
* @param string $entity Name of the entity (e,g table)
* @param string $old_id
* @param string $new_id
*/
public function addMapping($entity, $old_id, $new_id)
{
$key_name = $entity.'_'.$old_id;
if (!$this->valueStore->has($key_name)) {
$this->valueStore->put($key_name, $new_id);
}
}
/**
* Return the ID for a mapping
*
* @param $entity
* @param $old_id
*
* @return bool
*/
public function getMapping($entity, $old_id)
{
$key_name = $entity.'_'.$old_id;
if (!$this->valueStore->has($key_name)) {
return 0;
}
return $this->valueStore->get($key_name);
}
/**
* Clear the value store
*/
public function clear()
{
$this->valueStore->flush();
}
}

256
app/Utils/ImporterDB.php Normal file
View File

@@ -0,0 +1,256 @@
<?php
namespace App\Utils;
use Illuminate\Support\Facades\Log;
use PDO;
use PDOException;
/**
* Real basic to interface with an importer
*/
class ImporterDB
{
/**
* @var int
*/
public $batchSize;
/**
* @var PDO
*/
private $conn;
/**
* @var string
*/
private $dsn;
/**
* @var array
*/
private $creds;
public function __construct($creds)
{
$this->creds = $creds;
$this->dsn = 'mysql:'.implode(';', [
'host='.$this->creds['host'],
'port='.$this->creds['port'],
'dbname='.$this->creds['name'],
]);
Log::info('Using DSN: '.$this->dsn);
$this->batchSize = config('installer.importer.batch_size', 20);
}
public function __destruct()
{
$this->close();
}
public function connect()
{
try {
$this->conn = new PDO($this->dsn, $this->creds['user'], $this->creds['pass']);
$this->conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
} catch (PDOException $e) {
Log::error($e);
throw $e;
}
}
public function close()
{
if ($this->conn) {
$this->conn = null;
}
}
/**
* Return the table name with the prefix
*
* @param $table
*
* @return string
*/
public function tableName($table)
{
if ($this->creds['table_prefix'] !== false) {
return $this->creds['table_prefix'].$table;
}
return $table;
}
/**
* Does a table exist? Try to get the column information on it.
* The result will be 'false' if that table isn't there
*
* @param $table
*
* @return bool
*/
public function tableExists($table): bool
{
$this->connect();
$sql = 'SHOW COLUMNS FROM '.$this->tableName($table);
$result = $this->conn->query($sql);
if (!$result) {
return false;
}
return true;
}
/**
* Get the names of the columns for a particular table
*
* @param $table
*
* @return mixed
*/
public function getColumns($table)
{
$this->connect();
$sql = 'SHOW COLUMNS FROM '.$this->tableName($table);
$result = $this->conn->query($sql)->fetchAll();
$rows = [];
foreach ($result as $row) {
$rows[] = $row->Field;
}
return $rows;
}
/**
* @param $table
*
* @return mixed
*/
public function getTotalRows($table)
{
$this->connect();
$sql = 'SELECT COUNT(*) FROM '.$this->tableName($table);
$rows = $this->conn->query($sql)->fetchColumn();
Log::info('Found '.$rows.' rows in '.$table);
return (int) $rows;
}
/**
* Read rows from a table with a given assoc array. Simple
*
* @param string $table
* @param array $attrs
*
* @return false|\PDOStatement
*/
public function findBy($table, array $attrs)
{
$this->connect();
$where = [];
foreach ($attrs as $col => $value) {
$where[] = $col.'=\''.$value.'\'';
}
$where = implode(' AND ', $where);
$sql = implode(' ', [
'SELECT',
'*',
'FROM',
$this->tableName($table),
'WHERE',
$where,
]);
return $this->conn->query($sql);
}
/**
* Read all the rows in a table, but read them in a batched manner
*
* @param string $table The name of the table
* @param string $order_by Column to order by
* @param int $start_offset
* @param string $fields
*
* @return array
*/
public function readRows($table, $order_by = 'id', $start_offset = 0, $fields = '*')
{
$this->connect();
$offset = $start_offset;
// $total_rows = $this->getTotalRows($table);
$rows = [];
$result = $this->readRowsOffset($table, $this->batchSize, $offset, $order_by, $fields);
if ($result === false || $result === null) {
return [];
}
try {
foreach ($result as $row) {
$rows[] = $row;
}
} catch (\Exception $e) {
Log::error('foreach rows error: '.$e->getMessage());
}
return $rows;
}
/**
* @param string $table
* @param int $limit Number of rows to read
* @param int $offset Where to start from
* @param $order_by
* @param string $fields
*
* @return false|\PDOStatement|void
*/
public function readRowsOffset($table, $limit, $offset, $order_by, $fields = '*')
{
if (is_array($fields)) {
$fields = implode(',', $fields);
}
$sql = implode(' ', [
'SELECT',
$fields,
'FROM',
$this->tableName($table),
'ORDER BY '.$order_by.' ASC',
'LIMIT '.$limit,
'OFFSET '.$offset,
]);
try {
$result = $this->conn->query($sql);
if (!$result || $result->rowCount() === 0) {
return;
}
return $result;
} catch (PDOException $e) {
// Without incrementing the offset, it should re-run the same query
Log::error('Error readRowsOffset: '.$e->getMessage());
if (strpos($e->getMessage(), 'server has gone away') !== false) {
$this->connect();
}
} catch (\Exception $e) {
Log::error('Error readRowsOffset: '.$e->getMessage());
}
}
}