chore(operations): remove legacy-era platform-operations sources

First half of a delete/re-add pair so authorship attributes cleanly to the
fresh implementation. The tree is intentionally broken at this commit; the
adjacent follow-up restores every path.
This commit is contained in:
Darko Gjorgjijoski
2026-08-20 18:30:12 +02:00
parent 1f45b3a8d0
commit a1828fac52
31 changed files with 0 additions and 2569 deletions
@@ -1,89 +0,0 @@
<?php
namespace App\Platform\Operations\Console;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Support\Facades\Artisan;
use function Laravel\Prompts\confirm;
class ResetApp extends Command
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:app {--force}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clean database and public/storage folder';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
/**
* Execute the console command to reset the application.
*
* This will:
* 1. Enable maintenance mode to prevent access during reset
* 2. Fresh migrate the database with initial seeds
* 3. Seed demo data using DemoSeeder
* 4. Clear all application caches
* 5. Disable maintenance mode
*
* The --force flag can be used to skip confirmation prompt.
*/
public function handle(): void
{
if (! $this->option('force')) {
if (! confirm('Are you sure you want to reset the application?')) {
$this->components->error('Reset cancelled');
return;
}
}
// Enable maintenance mode to prevent access during reset
$this->info('Activating maintenance mode...');
Artisan::call('down');
// Fresh migrate database and run initial seeds
$this->info('Running migrate:fresh');
Artisan::call('migrate:fresh --seed --force');
// Seed demo data
$this->info('Seeding database');
Artisan::call('db:seed', ['--class' => 'DemoSeeder', '--force' => true]);
// Clear all application caches
$this->info('Clearing cache...');
Artisan::call('optimize:clear');
// Disable maintenance mode
$this->info('Deactivating maintenance mode...');
Artisan::call('up');
$this->info('App reset completed successfully!');
}
}
@@ -1,247 +0,0 @@
<?php
namespace App\Platform\Operations\Console;
use App\Platform\Operations\Update\Updater;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
// Implementation taken from Akaunting - https://github.com/akaunting/akaunting
class UpdateCommand extends Command
{
public $installed;
public $version;
public $response;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'core:update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Automatically update your InvoiceShelf Core App';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle(): void
{
set_time_limit(3600); // 1 hour
if (config('invoiceshelf.containerized')) {
$this->error('The in-app updater is disabled in containerized installs. Upgrade with `docker compose pull`.');
return;
}
$this->installed = $this->getInstalledVersion();
$this->response = $this->getLatestVersionResponse();
$this->version = ($this->response) ? $this->response->version : false;
if ($this->response == 'extension_required') {
$this->info('Sorry! Your system does not meet the minimum requirements for this update.');
$this->info('Please retry after installing the required version/extensions.');
return;
}
if (! $this->version) {
$this->info('No Update Available! You are already on the latest version.');
return;
}
if (! $this->confirm("Do you wish to update to {$this->version}?")) {
return;
}
if (! $path = $this->download()) {
return;
}
if (! $path = $this->unzip($path)) {
return;
}
if (! $this->copyFiles($path)) {
return;
}
if (isset($this->response->deleted_files) && ! empty($this->response->deleted_files)) {
if (! $this->deleteFiles($this->response->deleted_files)) {
return;
}
}
if (! $this->migrateUpdate()) {
return;
}
if (! $this->finish()) {
return;
}
$this->info('Successfully updated to '.$this->version);
}
public function getInstalledVersion()
{
return preg_replace('~[\r\n]+~', '', File::get(base_path('version.md')));
}
public function getLatestVersionResponse()
{
$this->info('Your currently installed version is '.$this->installed);
$this->line('');
$this->info('Checking for update...');
try {
$response = Updater::checkForUpdate($this->installed);
if ($response->success) {
$extensions = $response->version->extensions;
$is_required = false;
foreach ($extensions as $key => $extension) {
if (! $extension) {
$is_required = true;
$this->info('❌ '.$key);
}
$this->info('✅ '.$key);
}
if ($is_required) {
return 'extension_required';
}
return $response->version;
}
return false;
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
}
public function download()
{
$this->info('Downloading update...');
try {
$path = Updater::download($this->version, 1);
if (! is_string($path)) {
$this->error('Download exception');
return false;
}
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return $path;
}
public function unzip($path)
{
$this->info('Unzipping update package...');
try {
$path = Updater::unzip($path);
if (! is_string($path)) {
$this->error('Unzipping exception');
return false;
}
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return $path;
}
public function copyFiles($path)
{
$this->info('Copying update files...');
try {
Updater::copyFiles($path);
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return true;
}
public function deleteFiles($files)
{
$this->info('Deleting unused old files...');
try {
Updater::deleteFiles($files);
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return true;
}
public function migrateUpdate()
{
$this->info('Running Migrations...');
try {
Updater::migrateUpdate();
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return true;
}
public function finish()
{
$this->info('Finishing update...');
try {
Updater::finishUpdate($this->installed, $this->version);
} catch (\Exception $e) {
$this->error($e->getMessage());
return false;
}
return true;
}
}
@@ -1,25 +0,0 @@
<?php
namespace App\Platform\Operations\Events;
use Illuminate\Foundation\Events\Dispatchable;
class UpdateFinished
{
use Dispatchable;
public $new;
public $old;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($old, $new)
{
$this->old = $old;
$this->new = $new;
}
}
@@ -1,35 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Admin;
use App\Platform\Http\Controller;
use App\Platform\Operations\Http\Requests\GetSettingRequest;
use App\Platform\Operations\Http\Requests\SettingRequest;
use App\Platform\Operations\Models\Setting;
use Illuminate\Http\JsonResponse;
class SettingsController extends Controller
{
public function show(GetSettingRequest $request): JsonResponse
{
$this->authorize('manage settings');
$setting = Setting::getSetting($request->key);
return response()->json([
$request->key => $setting,
]);
}
public function update(SettingRequest $request): JsonResponse
{
$this->authorize('manage settings');
Setting::setSettings($request->settings);
return response()->json([
'success' => true,
$request->settings,
]);
}
}
@@ -1,116 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Admin;
use App\Platform\Http\Controller;
use App\Platform\Operations\Update\Updater;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class UpdateController extends Controller
{
public function checkVersion(Request $request): JsonResponse
{
$this->ensureSuperAdmin();
set_time_limit(600);
$channel = $request->get('channel', 'stable');
$version = preg_replace('~[\r\n]+~', '', File::get(base_path('version.md')));
return response()->json(Updater::checkForUpdate($version, $channel));
}
public function download(Request $request): JsonResponse
{
$this->ensureSuperAdmin();
$request->validate(['version' => 'required']);
return response()->json([
'success' => true,
'path' => Updater::download($request->version),
]);
}
public function unzip(Request $request): JsonResponse
{
$this->ensureSuperAdmin();
$request->validate(['path' => 'required']);
try {
return response()->json([
'success' => true,
'path' => Updater::unzip($request->path),
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage(),
], 500);
}
}
public function copy(Request $request): JsonResponse
{
$this->ensureSuperAdmin();
$request->validate(['path' => 'required']);
return response()->json([
'success' => true,
'path' => Updater::copyFiles($request->path),
]);
}
public function delete(Request $request): JsonResponse
{
return $this->clean($request);
}
public function clean(Request $request): JsonResponse
{
$this->ensureSuperAdmin();
// Backward compatibility: use deleted_files when no manifest exists
if (! File::exists(base_path('manifest.json'))
&& isset($request->deleted_files)
&& ! empty($request->deleted_files)) {
Updater::deleteFiles($request->deleted_files);
return response()->json(['success' => true, 'cleaned' => 0]);
}
$result = Updater::cleanStaleFiles();
return response()->json($result);
}
public function migrate(Request $request): JsonResponse
{
$this->ensureSuperAdmin();
Updater::migrateUpdate();
return response()->json(['success' => true]);
}
public function finish(Request $request): JsonResponse
{
$this->ensureSuperAdmin();
$request->validate([
'installed' => 'required',
'version' => 'required',
]);
return response()->json(Updater::finishUpdate($request->installed, $request->version));
}
private function ensureSuperAdmin(): void
{
$this->authorize('manage update app');
}
}
@@ -1,34 +0,0 @@
<?php
namespace App\Platform\Operations\Http;
use App\Platform\Http\Controller;
use App\Platform\Operations\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class AppVersionController extends Controller
{
/**
* Handle the incoming request.
*
* @return JsonResponse
*/
public function __invoke(Request $request)
{
$version = preg_replace('~[\r\n]+~', '', File::get(base_path('version.md')));
$channel = Setting::getSetting('updater_channel');
if (is_null($channel)) {
$channel = 'stable';
Setting::setSetting('updater_channel', 'stable'); // default.
}
return response()->json([
'version' => $version,
'channel' => $channel,
'containerized' => (bool) config('invoiceshelf.containerized'),
]);
}
}
@@ -1,155 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Company;
use App\Domains\Accounts\Http\Resources\CompanyInvitationResource;
use App\Domains\Accounts\Http\Resources\CompanyResource;
use App\Domains\Accounts\Http\Resources\UserResource;
use App\Domains\Accounts\Models\Company;
use App\Domains\Accounts\Models\CompanyInvitation;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Money\Models\Currency;
use App\Platform\Http\Controller;
use App\Platform\Modules\Models\Module;
use App\Platform\Operations\Http\Concerns\GeneratesMenu;
use App\Platform\Operations\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Modules\Registry as ModuleRegistry;
use Silber\Bouncer\BouncerFacade;
class BootstrapController extends Controller
{
use GeneratesMenu;
/**
* Handle the incoming request.
*
* @return JsonResponse
*/
public function __invoke(Request $request)
{
$current_user = $request->user();
$current_user_settings = $current_user->getAllSettings();
$companies = $current_user->companies;
$pendingInvitations = CompanyInvitation::forUser($current_user)
->pending()
->with(['company', 'role', 'invitedBy'])
->get();
$global_settings = Setting::getSettings([
'admin_portal_theme',
'admin_portal_logo',
'login_page_logo',
'login_page_heading',
'login_page_description',
'admin_page_title',
'copyright_text',
'save_pdf_to_disk',
'show_sidebar_group_labels',
]);
// Super admin mode — return admin-only menu with all companies listed
if ($current_user->isSuperAdmin() && $request->has('admin_mode')) {
return response()->json([
'current_user' => new UserResource($current_user),
'current_user_settings' => $current_user_settings,
'current_user_abilities' => [],
'companies' => CompanyResource::collection($companies),
'current_company' => null,
'current_company_settings' => [],
'current_company_currency' => Currency::first(),
'config' => config('invoiceshelf'),
'global_settings' => $global_settings,
'main_menu' => $this->generateMenu('admin_menu', $current_user),
'setting_menu' => [],
'modules' => [],
'admin_mode' => true,
'pending_invitations' => CompanyInvitationResource::collection($pendingInvitations),
]);
}
// User has no companies — return minimal bootstrap
if ($companies->isEmpty()) {
return response()->json([
'current_user' => new UserResource($current_user),
'current_user_settings' => $current_user_settings,
'current_user_abilities' => [],
'companies' => [],
'current_company' => null,
'current_company_settings' => [],
'current_company_currency' => Currency::first(),
'config' => config('invoiceshelf'),
'global_settings' => $global_settings,
'main_menu' => [],
'setting_menu' => [],
'modules' => [],
'pending_invitations' => CompanyInvitationResource::collection($pendingInvitations),
]);
}
$main_menu = $this->generateMenu('main_menu', $current_user);
$setting_menu = $this->generateMenu('setting_menu', $current_user);
// Merge module-registered menu items into the main menu so they
// participate in the unified group + priority ordering.
foreach (ModuleRegistry::allMenu() as $slug => $item) {
$main_menu[] = [
'title' => __($item['title']),
'link' => $item['link'],
'icon' => $item['icon'],
'name' => 'module-'.$slug,
'group' => $item['group'] ?? 'modules',
'group_label' => $item['group_label'] ?? 'navigation.modules',
'priority' => $item['priority'] ?? 100,
];
}
$current_company = Company::find($request->header('company'));
if ((! $current_company) || ($current_company && ! $current_user->hasCompany($current_company->id))) {
$current_company = $current_user->companies()->first();
}
$current_company_settings = CompanySetting::getAllSettings($current_company->id);
$current_company_currency = $current_company_settings->has('currency')
? Currency::find($current_company_settings->get('currency'))
: Currency::first();
BouncerFacade::refreshFor($current_user);
return response()->json([
'current_user' => new UserResource($current_user),
'current_user_settings' => $current_user_settings,
'current_user_abilities' => $current_user->getAbilities(),
'companies' => CompanyResource::collection($companies),
'current_company' => new CompanyResource($current_company),
'current_company_settings' => $current_company_settings,
'current_company_currency' => $current_company_currency,
'config' => config('invoiceshelf'),
'global_settings' => $global_settings,
'main_menu' => $main_menu,
'setting_menu' => $setting_menu,
'modules' => Module::where('enabled', true)->pluck('name'),
'user_menu' => collect(ModuleRegistry::allUserMenu())
->map(fn (array $item, string $slug) => [
...$item,
'title' => __($item['title']),
'name' => 'module-'.$slug,
])
->sortBy('priority')
->values()
->all(),
'pending_invitations' => CompanyInvitationResource::collection($pendingInvitations),
]);
}
public function currentCompany(Request $request)
{
$company = Company::find($request->header('company'));
return new CompanyResource($company);
}
}
@@ -1,49 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Company;
use App\Platform\Http\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Modules\Registry;
class ConfigController extends Controller
{
/**
* Handle the incoming request.
*/
public function __invoke(Request $request): JsonResponse
{
if ($request->key === 'exchange_rate_drivers') {
return response()->json([
'exchange_rate_drivers' => $this->exchangeRateDrivers(),
]);
}
return response()->json([
$request->key => config('invoiceshelf.'.$request->key),
]);
}
/**
* Build the exchange rate driver list from the module Registry.
*
* Returns enriched objects (with label, website, and config_fields) so the
* frontend can render driver-specific configuration forms without hardcoding
* any per-driver UI.
*
* @return array<int, array<string, mixed>>
*/
protected function exchangeRateDrivers(): array
{
return collect(Registry::allDrivers('exchange_rate'))
->map(fn (array $meta, string $name) => [
'value' => $name,
'label' => $meta['label'] ?? $name,
'website' => $meta['website'] ?? '',
'config_fields' => $meta['config_fields'] ?? [],
])
->values()
->all();
}
}
@@ -1,30 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Concerns;
trait GeneratesMenu
{
public function generateMenu($key, $user)
{
$new_items = [];
$menu = \Menu::get($key);
$items = $menu ? $menu->items->toArray() : [];
foreach ($items as $data) {
if ($user->checkAccess($data)) {
$new_items[] = [
'title' => $data->title,
'link' => $data->link->path['url'],
'icon' => $data->data['icon'],
'name' => $data->data['name'],
'group' => $data->data['group'],
'group_label' => $data->data['group_label'] ?? '',
'priority' => $data->data['priority'] ?? 100,
];
}
}
return $new_items;
}
}
@@ -1,24 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CronJobMiddleware
{
/**
* Handle an incoming request.
*
* @return mixed
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->header('x-authorization-token') && $request->header('x-authorization-token') == config('services.cron_job.auth_token')) {
return $next($request);
}
return response()->json(['unauthorized'], 401);
}
}
@@ -1,29 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class GetSettingRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'key' => [
'required',
'string',
],
];
}
}
@@ -1,28 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SettingRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'settings' => [
'required',
],
];
}
}
@@ -1,23 +0,0 @@
<?php
namespace App\Platform\Operations\Http\Webhooks;
use App\Platform\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Artisan;
class CronJobController extends Controller
{
/**
* Handle the incoming request.
*
* @return Response
*/
public function __invoke(Request $request)
{
Artisan::call('schedule:run');
return response()->json(['success' => true]);
}
}
@@ -1,314 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Application;
use App\Platform\Operations\Installation\Http\Requests\DatabaseEnvironmentRequest;
use App\Platform\Operations\Installation\Http\Requests\DomainEnvironmentRequest;
use Exception;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
class EnvironmentManager
{
/**
* @var string
*/
private $envPath;
/**
* @var string
*/
private $delimiter = "\n";
/**
* Set the .env and .env.example paths.
*/
public function __construct($path = null)
{
$this->envPath = base_path('.env');
}
/**
* Returns the .env contents
*
* @return false|string
*/
private function getEnvContents()
{
return file_get_contents($this->envPath);
}
/**
* Updates .env file - inspired by Akaunting
*
* @return bool
*/
public function updateEnv(array $data)
{
if (empty($data) || ! is_array($data) || ! is_file($this->envPath)) {
return false;
}
$env = $this->getEnvContents();
$env = explode($this->delimiter, $env);
foreach ($data as $data_key => $data_value) {
$updated = false;
foreach ($env as $env_key => $env_value) {
$entry = explode('=', $env_value, 2);
// Check if new or old key
if ($entry[0] == $data_key) {
$env[$env_key] = sprintf('%s=%s', $data_key, $this->encode($data_value));
$updated = true;
}
}
// Lets create if not available
if (! $updated) {
$env[] = $data_key.'='.$this->encode($data_value);
}
}
$env = implode($this->delimiter, $env);
file_put_contents(base_path('.env'), $env);
return true;
}
/**
* Encodes value for .env
*
* @return mixed|string
*/
private function encode($str)
{
// Convert to string if not already
$str = (string) $str;
// If the value is already properly quoted, return as is
if (strlen($str) >= 2 && $str[0] === '"' && $str[strlen($str) - 1] === '"') {
return $str;
}
// Check if the value contains characters that need quoting
// Using a character class regex to properly match special characters
$specialChars = '\^\'£$%&*()}{@#~?><,|=\-_+¬!';
$needsQuoting = (
strpos($str, ' ') !== false ||
preg_match('/['.preg_quote($specialChars, '/').']/', $str)
);
if ($needsQuoting) {
// Escape any existing double quotes in the string
$str = str_replace('"', '\\"', $str);
$str = '"'.$str.'"';
}
return $str;
}
/**
* Save the database content to the .env file.
*
* @return array
*/
public function saveDatabaseVariables(DatabaseEnvironmentRequest $request)
{
$appUrl = $request->get('app_url');
if ($appUrl !== config('app.url')) {
config(['app.url' => $appUrl]);
}
[$sanctumDomain, $sessionDomain] = $this->getDomains(
$request->getHttpHost()
);
$dbEnv = [
'APP_URL' => $appUrl,
'APP_LOCALE' => $request->get('app_locale'),
'DB_CONNECTION' => $request->get('database_connection'),
'SESSION_DOMAIN' => $sessionDomain,
];
if ($sanctumDomain !== null) {
$dbEnv['SANCTUM_STATEFUL_DOMAINS'] = $sanctumDomain;
}
if ($dbEnv['DB_CONNECTION'] != 'sqlite') {
if ($request->has('database_username') && $request->has('database_password')) {
$dbEnv['DB_HOST'] = $request->get('database_hostname');
$dbEnv['DB_PORT'] = $request->get('database_port');
$dbEnv['DB_DATABASE'] = $request->get('database_name');
$dbEnv['DB_USERNAME'] = $request->get('database_username');
$dbEnv['DB_PASSWORD'] = $request->get('database_password');
}
} else {
// Laravel 11 requires SQLite at least v3.35.0
// https://laravel.com/docs/11.x/database#introduction
if (extension_loaded('sqlite3') && class_exists('\SQLite3') && method_exists('\SQLite3', 'version')) {
$version = \SQLite3::version();
if (! empty($version['versionString']) && version_compare($version['versionString'], '3.35.0', '<')) {
return [
'error_message' => sprintf('The minimum SQLite version is %s. Your current SQLite version is %s which is not supported. Please upgrade SQLite and retry.', '3.35.0', $version['versionString']),
];
}
} else {
return [
'error_message' => sprintf('SQLite3 is not present. Please install SQLite >=%s and retry.', '3.35.0'),
];
}
$dbEnv['DB_DATABASE'] = $request->get('database_name');
$sqlitePath = $this->resolveSqliteDatabasePath($dbEnv['DB_DATABASE']);
// Create empty SQLite database if it doesn't exist. Ensure the
// parent directory exists first so user-supplied absolute paths
// (e.g. /var/data/foo.sqlite) work even when the directory hasn't
// been pre-created.
if (! file_exists($sqlitePath)) {
$parentDir = dirname($sqlitePath);
if (! is_dir($parentDir)) {
mkdir($parentDir, 0755, true);
}
copy(database_path('stubs/sqlite.empty.db'), $sqlitePath);
}
}
try {
$this->checkDatabaseConnection($request);
if ($request->get('database_overwrite')) {
Artisan::call('db:wipe --force');
}
if (\Schema::hasTable('users')) {
return [
'error' => 'database_should_be_empty',
];
}
} catch (Exception $e) {
return [
'error_message' => $e->getMessage(),
];
}
try {
$this->updateEnv($dbEnv);
} catch (Exception $e) {
return [
'error' => 'database_variables_save_error',
];
}
return [
'success' => 'database_variables_save_successfully',
];
}
/**
* Returns PDO object if all ok.
*
* @return \Closure|\PDO
*/
private function checkDatabaseConnection(DatabaseEnvironmentRequest $request)
{
$connection = $request->get('database_connection');
$settings = config("database.connections.$connection");
$connectionArray = array_merge($settings, [
'driver' => $connection,
'database' => $connection === 'sqlite'
? $this->resolveSqliteDatabasePath($request->get('database_name'))
: $request->get('database_name'),
]);
if ($connection !== 'sqlite' && $request->has('database_username') && $request->has('database_password')) {
$connectionArray = array_merge($connectionArray, [
'username' => $request->get('database_username'),
'password' => $request->get('database_password'),
'host' => $request->get('database_hostname'),
'port' => $request->get('database_port'),
]);
}
config([
'database' => [
'migrations' => 'migrations',
'default' => $connection,
'connections' => [$connection => $connectionArray],
],
]);
DB::purge($connection);
return DB::connection($connection)->getPdo();
}
private function resolveSqliteDatabasePath(?string $databasePath): string
{
$databasePath = trim((string) $databasePath);
if ($databasePath === '') {
return storage_path('app/database.sqlite');
}
if ($this->isAbsolutePath($databasePath)) {
return $databasePath;
}
return base_path($databasePath);
}
private function isAbsolutePath(string $path): bool
{
return str_starts_with($path, DIRECTORY_SEPARATOR)
|| preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) === 1;
}
/**
* Save sanctum stateful domain to the .env file.
*
* @return array
*/
public function saveDomainVariables(DomainEnvironmentRequest $request)
{
try {
[$sanctumDomain, $sessionDomain] = $this->getDomains(
$request->get('app_domain')
);
$domainEnv = [
'SESSION_DOMAIN' => $sessionDomain,
];
if ($sanctumDomain !== null) {
$domainEnv['SANCTUM_STATEFUL_DOMAINS'] = $sanctumDomain;
}
$this->updateEnv($domainEnv);
} catch (Exception $e) {
return [
'error' => 'domain_verification_failed',
];
}
return [
'success' => 'domain_variable_save_successfully',
];
}
private function getDomains(string $requestDomain): array
{
$appUrl = config('app.url');
$port = parse_url($appUrl, PHP_URL_PORT);
$currentDomain = parse_url($appUrl, PHP_URL_HOST).(
$port ? ':'.$port : ''
);
$requestHost = parse_url($requestDomain, PHP_URL_HOST) ?: $requestDomain;
$isSame = $currentDomain === $requestDomain;
return [
$isSame && env('SANCTUM_STATEFUL_DOMAINS', false) === false ?
null : $requestDomain,
$isSame && env('SESSION_DOMAIN', false) === null ?
null : $requestHost,
];
}
}
@@ -1,73 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Application;
class FilePermissionChecker
{
/**
* @var array
*/
protected $results = [];
/**
* Set the result array permissions and errors.
*
* @return mixed
*/
public function __construct()
{
$this->results['permissions'] = [];
$this->results['errors'] = null;
}
/**
* Check for the folders permissions.
*
* @return array
*/
public function check(array $folders)
{
foreach ($folders as $folder => $permission) {
if (! ($this->getPermission($folder) >= $permission)) {
$this->addFileAndSetErrors($folder, $permission, false);
} else {
$this->addFile($folder, $permission, true);
}
}
return $this->results;
}
/**
* Get a folder permission.
*
* @return string
*/
private function getPermission($folder)
{
return substr(sprintf('%o', fileperms(base_path($folder))), -4);
}
/**
* Add the file to the list of results.
*/
private function addFile($folder, $permission, $isSet)
{
array_push($this->results['permissions'], [
'folder' => $folder,
'permission' => $permission,
'isSet' => $isSet,
]);
}
/**
* Add the file and set the errors.
*/
private function addFileAndSetErrors($folder, $permission, $isSet)
{
$this->addFile($folder, $permission, $isSet);
$this->results['errors'] = true;
}
}
@@ -1,235 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Application;
use Illuminate\Support\Str;
use PDO;
use SQLite3;
class RequirementsChecker
{
/**
* Minimum PHP Version Supported (Override is in installer.php config file).
*
* @var _minPhpVersion
*/
private $_minPhpVersion = '7.0.0';
/**
* Check for the server requirements.
*
* @return array
*/
public function check(array $requirements)
{
$results = [];
foreach ($requirements as $type => $requirement) {
switch ($type) {
// check php requirements
case 'php':
foreach ($requirements[$type] as $requirement) {
$results['requirements'][$type][$requirement] = true;
if (! extension_loaded($requirement)) {
$results['requirements'][$type][$requirement] = false;
$results['errors'] = true;
}
}
break;
// check apache requirements
case 'apache':
foreach ($requirements[$type] as $requirement) {
// if function doesn't exist we can't check apache modules
if (function_exists('apache_get_modules')) {
$results['requirements'][$type][$requirement] = true;
if (! in_array($requirement, apache_get_modules())) {
$results['requirements'][$type][$requirement] = false;
$results['errors'] = true;
}
}
}
break;
}
}
return $results;
}
/**
* Check PHP version requirement.
*
* @return array
*/
public function checkPHPVersion(?string $minPhpVersion = null)
{
$minVersionPhp = $minPhpVersion;
$currentPhpVersion = $this->getPhpVersionInfo();
$supported = false;
if ($minPhpVersion == null) {
$minVersionPhp = $this->getMinPhpVersion();
}
if (version_compare($currentPhpVersion['version'], $minVersionPhp) >= 0) {
$supported = true;
}
$phpStatus = [
'full' => $currentPhpVersion['full'],
'current' => $currentPhpVersion['version'],
'minimum' => $minVersionPhp,
'supported' => $supported,
];
return $phpStatus;
}
/**
* Get current Php version information.
*
* @return array
*/
private static function getPhpVersionInfo()
{
$currentVersionFull = PHP_VERSION;
preg_match("#^\d+(\.\d+)*#", $currentVersionFull, $filtered);
$currentVersion = $filtered[0];
return [
'full' => $currentVersionFull,
'version' => $currentVersion,
];
}
/**
* Get minimum PHP version ID.
*
* @return string _minPhpVersion
*/
protected function getMinPhpVersion()
{
return $this->_minPhpVersion;
}
/**
* Check PHP version requirement.
*
* @return array
*/
public function checkMysqlVersion($conn)
{
$version_info = $conn->getAttribute(PDO::ATTR_SERVER_VERSION);
$isMariaDb = Str::contains($version_info, 'MariaDB');
$minVersionMysql = $isMariaDb ? config('invoiceshelf.min_mariadb_version') : config('invoiceshelf.min_mysql_version');
$currentMysqlVersion = $this->getMysqlVersionInfo($conn);
$supported = false;
if (version_compare($currentMysqlVersion, $minVersionMysql) >= 0) {
$supported = true;
}
$phpStatus = [
'current' => $currentMysqlVersion,
'minimum' => $minVersionMysql,
'supported' => $supported,
];
return $phpStatus;
}
/**
* Get current Mysql version information.
*
* @return string
*/
private static function getMysqlVersionInfo($pdo)
{
$version = $pdo->query('select version()')->fetchColumn();
preg_match("/^[0-9\.]+/", $version, $match);
return $match[0];
}
/**
* Check Sqlite version requirement.
*
* @return array
*/
public function checkSqliteVersion(?string $minSqliteVersion = null)
{
$minVersionSqlite = $minSqliteVersion;
$currentSqliteVersion = $this->getSqliteVersionInfo();
$supported = false;
if (version_compare($currentSqliteVersion, $minVersionSqlite) >= 0) {
$supported = true;
}
$phpStatus = [
'current' => $currentSqliteVersion,
'minimum' => $minVersionSqlite,
'supported' => $supported,
];
return $phpStatus;
}
/**
* Get current Sqlite version information.
*
* @return string
*/
private static function getSqliteVersionInfo()
{
$currentVersion = SQLite3::version();
return $currentVersion['versionString'];
}
/**
* Check Pgsql version requirement.
*
* @return array
*/
public function checkPgsqlVersion($conn, ?string $minPgsqlVersion = null)
{
$minVersionPgsql = $minPgsqlVersion;
$currentPgsqlVersion = $this->getPgsqlVersionInfo($conn);
$supported = false;
if (version_compare($currentPgsqlVersion, $minVersionPgsql) >= 0) {
$supported = true;
}
$phpStatus = [
'current' => $currentPgsqlVersion,
'minimum' => $minVersionPgsql,
'supported' => $supported,
];
return $phpStatus;
}
/**
* Get current Pgsql version information.
*
* @return string
*/
private static function getPgsqlVersionInfo($conn)
{
$currentVersion = pg_version($conn);
return $currentVersion['server'];
}
}
@@ -1,28 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Controllers;
use App\Platform\Http\Controller;
use App\Platform\Operations\Installation\Application\EnvironmentManager;
use App\Platform\Operations\Installation\Http\Requests\DomainEnvironmentRequest;
use Illuminate\Support\Facades\Artisan;
class AppDomainController extends Controller
{
public function __invoke(DomainEnvironmentRequest $request)
{
Artisan::call('optimize:clear');
$environmentManager = new EnvironmentManager;
$results = $environmentManager->saveDomainVariables($request);
if (in_array('error', $results)) {
return response()->json($results);
}
return response()->json([
'success' => true,
]);
}
}
@@ -1,108 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Controllers;
use App\Platform\Http\Controller;
use App\Platform\Operations\Installation\Application\EnvironmentManager;
use App\Platform\Operations\Installation\Application\InstallationState;
use App\Platform\Operations\Installation\Http\Requests\DatabaseEnvironmentRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
class DatabaseConfigurationController extends Controller
{
/**
* @var EnvironmentManager
*/
protected $EnvironmentManager;
public function __construct(EnvironmentManager $environmentManager)
{
$this->environmentManager = $environmentManager;
}
public function saveDatabaseEnvironment(DatabaseEnvironmentRequest $request)
{
Artisan::call('config:clear');
Artisan::call('cache:clear');
$results = $this->environmentManager->saveDatabaseVariables($request);
if (array_key_exists('success', $results)) {
// Automatically regenerating the key is disabled to prevent complications in the wizard process.
// This can cause issues with the CSRF token, resulting in "Token Mismatch" or "Invalid CSRF Token" errors.
// It is recommended that the user manually generates the key before running the wizard to ensure application security and stability.
// Artisan::call('key:generate --force');
Artisan::call('optimize:clear');
Artisan::call('config:clear');
Artisan::call('cache:clear');
Artisan::call('storage:link');
Artisan::call('migrate --seed --force');
// Set version.
InstallationState::setCurrentVersion();
}
return response()->json($results);
}
public function getDatabaseEnvironment(Request $request)
{
$databaseData = [];
$connection = $request->connection ?? config('database.default');
switch ($connection) {
case 'sqlite':
$databaseData = [
'database_connection' => 'sqlite',
'database_name' => config('database.connections.sqlite.database') ?: 'storage/app/database.sqlite',
];
break;
case 'pgsql':
$databaseData = [
'database_connection' => 'pgsql',
'database_host' => '127.0.0.1',
'database_port' => 5432,
];
break;
case 'mysql':
$databaseData = [
'database_connection' => 'mysql',
'database_host' => '127.0.0.1',
'database_port' => 3306,
];
break;
case 'mariadb':
$databaseData = [
'database_connection' => 'mariadb',
'database_host' => '127.0.0.1',
'database_port' => 3306,
];
break;
default:
// Never return an empty config: the wizard picks its form from
// database_connection, so an unrecognised driver used to render
// a blank step with no way forward. Echo it back with the
// server defaults instead.
$databaseData = [
'database_connection' => $connection,
'database_host' => '127.0.0.1',
'database_port' => 3306,
];
break;
}
return response()->json([
'config' => $databaseData,
'success' => true,
]);
}
}
@@ -1,39 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Controllers;
use App\Platform\Http\Controller;
use App\Platform\Operations\Installation\Application\FilePermissionChecker;
use Illuminate\Http\JsonResponse;
class FilePermissionsController extends Controller
{
/**
* @var PermissionsChecker
*/
protected $permissions;
/**
* @param PermissionsChecker $checker
*/
public function __construct(FilePermissionChecker $checker)
{
$this->permissions = $checker;
}
/**
* Display the permissions check page.
*
* @return JsonResponse
*/
public function permissions()
{
$permissions = $this->permissions->check(
config('installer.permissions')
);
return response()->json([
'permissions' => $permissions,
]);
}
}
@@ -1,20 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Controllers;
use App\Platform\Http\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class FinishController extends Controller
{
/**
* Handle the incoming request.
*
* @return JsonResponse
*/
public function __invoke(Request $request)
{
return response()->json(['success' => true]);
}
}
@@ -1,50 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Controllers;
use App\Domains\Accounts\Models\User;
use App\Platform\Http\Controller;
use App\Platform\Operations\Installation\Authentication\InstallWizardAuth;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$user = User::where('role', 'super admin')->first();
if (! $user) {
return response()->json([
'message' => 'Super admin user not found.',
], 404);
}
$company = $user->companies()->first();
if (! $company) {
return response()->json([
'message' => 'Super admin company not found.',
], 422);
}
Auth::guard('web')->logout();
if ($request->hasSession()) {
$request->session()->invalidate();
$request->session()->regenerateToken();
}
$user->tokens()->where('name', InstallWizardAuth::TOKEN_NAME)->delete();
$token = $user->createToken(
InstallWizardAuth::TOKEN_NAME,
[InstallWizardAuth::TOKEN_ABILITY],
)->plainTextToken;
return response()->json([
'success' => true,
'type' => 'Bearer',
'token' => $token,
'user' => $user,
'company' => $company,
]);
}
}
@@ -1,58 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Controllers;
use App\Platform\Http\Controller;
use App\Platform\Operations\Installation\Application\InstallationState;
use App\Platform\Operations\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class OnboardingWizardController extends Controller
{
/**
* Handle the incoming request.
*
* @return JsonResponse
*/
public function getStep(Request $request)
{
if (! InstallationState::isDbCreated()) {
return response()->json([
'profile_complete' => 0,
'profile_language' => 'en',
]);
}
return response()->json([
'profile_complete' => Setting::getSetting('profile_complete'),
'profile_language' => Setting::getSetting('profile_language'),
]);
}
public function updateStep(Request $request)
{
$setting = Setting::getSetting('profile_complete');
if ($setting === 'COMPLETED') {
return response()->json([
'profile_complete' => $setting,
]);
}
Setting::setSetting('profile_complete', $request->profile_complete);
return response()->json([
'profile_complete' => Setting::getSetting('profile_complete'),
]);
}
public function saveLanguage(Request $request)
{
Setting::setSetting('profile_language', $request->profile_language);
return response()->json([
'profile_language' => Setting::getSetting('profile_language'),
]);
}
}
@@ -1,41 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Controllers;
use App\Platform\Http\Controller;
use App\Platform\Operations\Installation\Application\RequirementsChecker;
use Illuminate\Http\JsonResponse;
class RequirementsController extends Controller
{
/**
* @var RequirementsChecker
*/
protected $requirements;
public function __construct(RequirementsChecker $checker)
{
$this->requirements = $checker;
}
/**
* Display the requirements page.
*
* @return JsonResponse
*/
public function requirements()
{
$phpSupportInfo = $this->requirements->checkPHPVersion(
config('installer.core.minPhpVersion')
);
$requirements = $this->requirements->check(
config('installer.requirements')
);
return response()->json([
'phpSupportInfo' => $phpSupportInfo,
'requirements' => $requirements,
]);
}
}
@@ -1,30 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Middleware;
use App\Platform\Operations\Installation\Application\InstallationState;
use App\Platform\Operations\Models\Setting;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureInstalled
{
/**
* Handle an incoming request.
*
* @return mixed
*/
public function handle(Request $request, Closure $next): Response
{
try {
if (! InstallationState::isDbCreated() || Setting::getSetting('profile_complete') !== 'COMPLETED') {
return redirect('/installation');
}
} catch (\Exception $e) {
return redirect('/installation');
}
return $next($request);
}
}
@@ -1,32 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Middleware;
use App\Platform\Operations\Installation\Application\InstallationState;
use App\Platform\Operations\Models\Setting;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfInstalled
{
/**
* Handle an incoming request.
*
* @return mixed
*/
public function handle(Request $request, Closure $next): Response
{
if (InstallationState::isDbCreated()) {
try {
if (Setting::getSetting('profile_complete') === 'COMPLETED') {
return redirect('login');
}
} catch (\Exception $e) {
// Settings table may not exist yet during installation
}
}
return $next($request);
}
}
@@ -1,80 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DatabaseEnvironmentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
switch ($this->get('database_connection')) {
case 'sqlite':
return [
'app_url' => [
'required',
'url',
],
'database_connection' => [
'required',
'string',
],
'database_name' => [
'required',
'string',
],
'database_overwrite' => [
'nullable',
'boolean',
],
];
break;
default:
return [
'app_url' => [
'required',
'url',
],
'database_connection' => [
'required',
'string',
],
'database_hostname' => [
'required',
'string',
],
'database_port' => [
'required',
'numeric',
],
'database_name' => [
'required',
'string',
],
'database_username' => [
'required',
'string',
],
'database_overwrite' => [
'nullable',
'boolean',
],
];
break;
}
}
}
@@ -1,28 +0,0 @@
<?php
namespace App\Platform\Operations\Installation\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DomainEnvironmentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'app_domain' => [
'required',
],
];
}
}
@@ -1,79 +0,0 @@
<?php
namespace App\Platform\Operations\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class Setting extends Model
{
protected $table = 'settings';
use HasFactory;
protected $fillable = ['option', 'value'];
/**
* Create or update a single application setting by key.
*/
public static function setSetting(string $key, mixed $setting): void
{
$old = self::whereOption($key)->first();
if ($old) {
$old->value = $setting;
$old->save();
return;
}
$set = new Setting;
$set->option = $key;
$set->value = $setting;
$set->save();
}
/**
* Bulk create or update application settings from a key-value array.
*/
public static function setSettings(array $settings): void
{
foreach ($settings as $key => $value) {
self::updateOrCreate(
[
'option' => $key,
],
[
'option' => $key,
'value' => $value,
]
);
}
}
/**
* Retrieve a single setting value by key, or null if not found.
*/
public static function getSetting(string $key): mixed
{
$setting = static::whereOption($key)->first();
if ($setting) {
return $setting->value;
} else {
return null;
}
}
/**
* Retrieve multiple settings as a key-value collection.
*/
public static function getSettings(array $settings): Collection
{
return static::whereIn('option', $settings)
->get()->mapWithKeys(function ($item) {
return [$item['option'] => $item['value']];
});
}
}
@@ -1,35 +0,0 @@
<?php
namespace App\Platform\Operations\Update;
use App\Platform\Operations\Models\Setting;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
// Implementation taken from Akaunting - https://github.com/akaunting/akaunting
trait CallsReleaseServer
{
protected static function getRemote($url, $data = [], $token = null)
{
$client = new Client(['verify' => true, 'base_uri' => config('invoiceshelf.base_url').'/']);
$headers['headers'] = [
'Accept' => 'application/json',
'Referer' => url('/'),
'Authorization' => "Bearer {$token}",
'invoiceshelf' => Setting::getSetting('version'),
];
$data['http_errors'] = false;
$data = array_merge($data, $headers);
try {
$result = $client->get($url, $data);
} catch (GuzzleException $e) {
$result = null;
}
return $result;
}
}
-225
View File
@@ -1,225 +0,0 @@
<?php
namespace App\Platform\Operations\Update;
use App\Platform\Operations\Events\UpdateFinished;
use App\Platform\Operations\Models\Setting;
use Artisan;
use File;
use GuzzleHttp\Exception\RequestException;
use ZipArchive;
// Implementation taken from Akaunting - https://github.com/akaunting/akaunting
class Updater
{
use CallsReleaseServer;
public static function checkForUpdate($installed_version, $updater_channel = 'stable')
{
$data = null;
$url = sprintf('releases/update-check/%s?channel=%s', $installed_version, $updater_channel);
$response = static::getRemote($url, ['timeout' => 100, 'track_redirects' => true]);
$data = (object) ['success' => false, 'release' => null];
if ($response && ($response->getStatusCode() == 200)) {
$data = $response->getBody()->getContents();
$data = json_decode($data);
}
if ($data->success && $data->release && property_exists($data->release, 'extensions')) {
$extensions = [];
foreach ($data->release->extensions as $extension) {
$extensions[$extension] = phpversion($extension) !== false;
}
$extensions['php'.'('.$data->release->min_php_version.')'] = version_compare(phpversion(), $data->release->min_php_version, '>=');
$data->release->extensions = $extensions;
}
return $data;
}
public static function download($new_version, $is_cmd = 0)
{
$data = null;
$path = null;
$url = 'releases/download/'.$new_version.'.zip';
$response = static::getRemote($url, ['timeout' => 100, 'track_redirects' => true]);
// Exception
if ($response instanceof RequestException) {
return [
'success' => false,
'error' => 'Download Exception',
'data' => [
'path' => $path,
],
];
}
if ($response && ($response->getStatusCode() == 200)) {
$data = $response->getBody()->getContents();
}
// Create temp directory
$temp_dir = storage_path('app/temp-'.md5(mt_rand()));
if (! File::isDirectory($temp_dir)) {
File::makeDirectory($temp_dir);
}
$zip_file_path = $temp_dir.'/upload.zip';
// Add content to the Zip file
$uploaded = is_int(file_put_contents($zip_file_path, $data)) ? true : false;
if (! $uploaded) {
return false;
}
return $zip_file_path;
}
public static function unzip($zip_file_path)
{
if (! file_exists($zip_file_path)) {
throw new \Exception('Zip file not found');
}
$temp_extract_dir = storage_path('app/temp2-'.md5(mt_rand()));
if (! File::isDirectory($temp_extract_dir)) {
File::makeDirectory($temp_extract_dir);
}
// Unzip the file
$zip = new ZipArchive;
if ($zip->open($zip_file_path)) {
$zip->extractTo($temp_extract_dir);
}
$zip->close();
// Delete zip file
File::delete($zip_file_path);
return $temp_extract_dir;
}
public static function copyFiles($temp_extract_dir)
{
if (! File::copyDirectory($temp_extract_dir.'/InvoiceShelf', base_path())) {
return false;
}
// Delete temp directory
File::deleteDirectory($temp_extract_dir);
return true;
}
public static function deleteFiles($json)
{
$files = json_decode($json);
foreach ($files as $file) {
File::delete(base_path($file));
}
return true;
}
public static function cleanStaleFiles(): array
{
$manifestPath = base_path('manifest.json');
if (! File::exists($manifestPath)) {
return ['success' => true, 'cleaned' => 0];
}
$manifest = json_decode(File::get($manifestPath), true);
if (! is_array($manifest)) {
return ['success' => false, 'error' => 'Invalid manifest'];
}
$manifestLookup = array_flip($manifest);
$protectedPaths = config('invoiceshelf.update_protected_paths', []);
$cleaned = 0;
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(base_path(), \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
$relativePath = substr($file->getPathname(), strlen(base_path()) + 1);
if (static::isProtectedPath($relativePath, $protectedPaths)) {
continue;
}
if ($file->isFile() && ! isset($manifestLookup[$relativePath])) {
File::delete($file->getPathname());
$cleaned++;
}
}
// Second pass: remove empty directories
$dirIterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(base_path(), \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($dirIterator as $item) {
if (! $item->isDir()) {
continue;
}
$relativePath = substr($item->getPathname(), strlen(base_path()) + 1);
if (static::isProtectedPath($relativePath, $protectedPaths)) {
continue;
}
$entries = scandir($item->getPathname());
if (count($entries) <= 2) {
@rmdir($item->getPathname());
}
}
return ['success' => true, 'cleaned' => $cleaned];
}
private static function isProtectedPath(string $relativePath, array $protectedPaths): bool
{
foreach ($protectedPaths as $protected) {
if ($relativePath === $protected || str_starts_with($relativePath, $protected.'/')) {
return true;
}
}
return false;
}
public static function migrateUpdate()
{
Artisan::call('migrate --force');
return true;
}
public static function finishUpdate($installed, $version)
{
Setting::setSetting('version', $version);
event(new UpdateFinished($installed, $version));
return [
'success' => true,
'error' => false,
'data' => [],
];
}
}
-210
View File
@@ -1,210 +0,0 @@
<?php
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Metadata\Models\CustomField;
use App\Domains\Money\Models\Currency;
use App\Platform\Operations\Installation\Application\InstallationState;
use App\Platform\Operations\Models\Setting;
use Illuminate\Support\Str;
/**
* Get company setting
*
* @return string
*/
function get_company_setting($key, $company_id)
{
if (! InstallationState::isDbCreated()) {
return null;
}
return CompanySetting::getSetting($key, $company_id);
}
/**
* Get app setting
*
* @param $company_id
* @return string
*/
function get_app_setting($key)
{
if (! InstallationState::isDbCreated()) {
return null;
}
return Setting::getSetting($key);
}
/**
* Get page title
*
* @return string
*/
function get_page_title($company_id)
{
if (! InstallationState::isDbCreated()) {
return null;
}
$routeName = Route::currentRouteName();
$defaultPageTitle = 'InvoiceShelf - Self Hosted Invoicing Platform';
if ($routeName === 'customer.dashboard') {
$pageTitle = CompanySetting::getSetting('customer_portal_page_title', $company_id);
return $pageTitle ? $pageTitle : $defaultPageTitle;
}
$pageTitle = Setting::getSetting('admin_page_title');
return $pageTitle ? $pageTitle : $defaultPageTitle;
}
/**
* Set Active Path
*
* @param string $active
* @return string
*/
function set_active($path, $active = 'active')
{
return call_user_func_array('Request::is', (array) $path) ? $active : '';
}
/**
* @return mixed
*/
function is_url($path)
{
return call_user_func_array('Request::is', (array) $path);
}
/**
* @return string
*/
function getCustomFieldValueKey(string $type)
{
switch ($type) {
case 'Input':
return 'string_answer';
case 'TextArea':
return 'string_answer';
case 'Phone':
return 'number_answer';
case 'Url':
return 'string_answer';
case 'Number':
return 'number_answer';
case 'Dropdown':
return 'string_answer';
case 'Switch':
return 'boolean_answer';
case 'Date':
return 'date_answer';
case 'Time':
return 'time_answer';
case 'DateTime':
return 'date_time_answer';
default:
return 'string_answer';
}
}
/**
* Format an amount given in cents as currency markup for PDF templates.
*
* The magnitude is formatted first and a single minus sign is prefixed to the
* whole assembled string, so a negative amount reads "-$24,738.00" rather than
* "$-24,738.00" and the symbol stays glued to the digits in both symbol
* positions. The symbol is wrapped in a DejaVu Sans span so it renders even
* when the active font has no glyph for it.
*
* @param int|float|string|null $money Amount in cents.
* @param Currency|null $currency Defaults to the company currency setting.
* @return string
*/
function format_money_pdf($money, $currency = null)
{
$money = $money / 100;
if (! $currency) {
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', 1));
}
$format_money = number_format(
abs($money),
$currency->precision,
$currency->decimal_separator,
$currency->thousand_separator
);
$symbol = '<span style="font-family: DejaVu Sans;">'.$currency->symbol.'</span>';
$currency_with_symbol = $currency->swap_currency_symbol
? $format_money.$symbol
: $symbol.$format_money;
// The sign is decided on the formatted digits, not on the raw input, so an
// amount that rounds away at the currency's precision (a stray cent on a
// zero-precision currency) renders as zero instead of "-0".
$is_negative = $money < 0 && preg_match('/[1-9]/', $format_money) === 1;
return $is_negative ? '-'.$currency_with_symbol : $currency_with_symbol;
}
/**
* @param $string
* @return string
*/
function clean_slug($model, $title, $id = 0)
{
// Normalize the title
$slug = Str::upper('CUSTOM_'.$model.'_'.Str::slug($title, '_'));
// Get any that could possibly be related.
// This cuts the queries down by doing it once.
$allSlugs = getRelatedSlugs($model, $slug, $id);
// If we haven't used it before then we are all good.
if (! $allSlugs->contains('slug', $slug)) {
return $slug;
}
// Just append numbers like a savage until we find not used.
for ($i = 1; $i <= 10; $i++) {
$newSlug = $slug.'_'.$i;
if (! $allSlugs->contains('slug', $newSlug)) {
return $newSlug;
}
}
throw new Exception('Can not create a unique slug');
}
function getRelatedSlugs($type, $slug, $id = 0)
{
return CustomField::select('slug')->where('slug', 'like', $slug.'%')
->where('model_type', $type)
->where('id', '<>', $id)
->get();
}
function respondJson($error, $message)
{
return response()->json([
'error' => $error,
'message' => $message,
], 422);
}