Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions ProcessMaker/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@
use Igaster\LaravelTheme\Facades\Theme;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\Application as IlluminateApplication;
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
use Illuminate\Foundation\Bootstrap\RegisterProviders;
use Illuminate\Foundation\PackageManifest;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use ProcessMaker\Multitenancy\Tenant;
use ProcessMaker\Multitenancy\TenantBootstrapper;

/**
* Class Application.
*/
class Application extends IlluminateApplication
{
public $overrideTenantId = null;

public $skipCacheEvents = false;

/**
* Sets the timezone for the application and for php with the specified timezone.
*
Expand Down Expand Up @@ -90,4 +101,15 @@ public function registerConfiguredProviders()

parent::registerConfiguredProviders();
}

public function bootstrapWith(array $bootstrappers)
{
// Insert TenantBootstrapper after LoadEnvironmentVariables
if ($bootstrappers[0] !== LoadEnvironmentVariables::class) {
throw new \Exception('LoadEnvironmentVariables is not the first bootstrapper. Did a laravel upgrade change this?');
}
array_splice($bootstrappers, 1, 0, [TenantBootstrapper::class]);

return parent::bootstrapWith($bootstrappers);
}
}
4 changes: 2 additions & 2 deletions ProcessMaker/Console/Commands/ProcessMakerLicenseRemove.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ class ProcessMakerLicenseRemove extends Command
*/
public function handle()
{
if (Storage::disk('root')->exists('license.json')) {
if (Storage::disk('local')->exists('license.json')) {
if ($this->option('force') || $this->confirm('Are you sure you want to remove the license.json file?')) {
Storage::disk('root')->delete('license.json');
Storage::disk('local')->delete('license.json');
$this->info('license.json removed successfully!');

$this->info('Calling package:discover to update the package cache with enabled packages');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function handle()
return 1;
}

Storage::disk('root')->put('license.json', $content);
Storage::disk('local')->put('license.json', $content);

$this->info('Calling package:discover to update the package cache with enabled packages');
Artisan::call('package:discover');
Expand Down
141 changes: 61 additions & 80 deletions ProcessMaker/Console/Commands/TenantsVerify.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
namespace ProcessMaker\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Models\EnvironmentVariable;
use ProcessMaker\Models\User;
use Spatie\Multitenancy\Models\Tenant;

class TenantsVerify extends Command
Expand All @@ -14,7 +19,7 @@ class TenantsVerify extends Command
*
* @var string
*/
protected $signature = 'tenants:verify {--verify-against= : The tenant ID to verify against}';
protected $signature = 'tenants:verify';

/**
* The console command description.
Expand All @@ -23,17 +28,6 @@ class TenantsVerify extends Command
*/
protected $description = 'Verify tenant configuration and storage paths';

/**
* Strip protocol from URL
*
* @param string $url
* @return string
*/
private function stripProtocol(string $url): string
{
return preg_replace('#^https?://#', '', $url);
}

/**
* Execute the console command.
*
Expand All @@ -46,85 +40,72 @@ public function handle()
$currentTenant = app('currentTenant');
}

$verifyAgainstId = $this->option('verify-against');

if (!$currentTenant) {
$this->error('No current tenant found');
if (config('app.multitenancy') && !$currentTenant) {
$this->error('Multitenancy enabled but no current tenant found.');

return;
}

$this->info('Current Tenant ID: ' . $currentTenant->id);
$this->line('----------------------------------------');
$this->info('Current Tenant ID: ' . ($currentTenant?->id ?? 'NONE'));

// Expected paths and configurations
$expectedStoragePath = base_path('storage/tenant_' . $currentTenant->id);
$actualConfigs = [
'filesystems.disks.local.root' => storage_path('app'),
'cache.prefix' => config('cache.prefix'),
'app.url' => config('app.url'),
'script-runner-microservice.callback' => config('script-runner-microservice.callback'),
$paths = [
['Storage Path', storage_path()],
['Config Cache Path', app()->getCachedConfigPath()],
['Lang Path', lang_path()],
];

// Display current values
$this->info('Current Storage Path: ' . storage_path());
$this->line('----------------------------------------');

$this->info('Current Configuration Values:');
foreach ($actualConfigs as $key => $expectedValue) {
$currentValue = config($key);
$this->line("{$key}: {$currentValue}");
}

// If verify-against is specified, perform verification
if ($verifyAgainstId) {
$this->line('----------------------------------------');
$this->info("Verifying against tenant ID: {$verifyAgainstId}");
// Display paths in a nice table
$this->table(['Path', 'Value'], $paths);

$configs = [
'app.key',
'app.url',
'app.instance',
'cache.prefix',
'database.redis.options.prefix',
'cache.stores.cache_settings.prefix',
'script-runner-microservice.callback',
'database.connections.processmaker.database',
'logging.channels.daily.path',
'filesystems.disks.public.root',
'filesystems.disks.local.root',
'filesystems.disks.lang.root',
];

$expectedStoragePath = base_path('storage/tenant_' . $verifyAgainstId);
$expectedConfigs = [
'filesystems.disks.local.root' => $expectedStoragePath . '/app',
'cache.prefix' => 'tenant_id_' . $verifyAgainstId,
'app.url' => config('app.url'),
$configs = array_map(function ($config) {
return [
$config,
config($config),
];

$hasMismatch = false;

// Verify storage path
if (storage_path() !== $expectedStoragePath) {
$this->error('Storage path mismatch!');
$this->line("Expected: {$expectedStoragePath}");
$this->line('Current: ' . storage_path());
$hasMismatch = true;
}

// Verify tenant URL if tenant exists
$verifyTenant = Tenant::find($verifyAgainstId);
if ($verifyTenant && $verifyTenant->domain !== $this->stripProtocol(config('app.url'))) {
$this->error('Tenant URL mismatch!');
$this->line("Expected: {$verifyTenant->domain}");
$this->line('Current: ' . config('app.url'));
$hasMismatch = true;
}

// Verify config values
foreach ($expectedConfigs as $key => $expectedValue) {
$currentValue = config($key);
if ($currentValue !== $expectedValue) {
$this->error("Config mismatch for {$key}!");
$this->line("Expected: {$expectedValue}");
$this->line("Current: {$currentValue}");
$hasMismatch = true;
}
}, $configs);

// Display configs in a nice table
$this->table(['Config', 'Value'], $configs);

$env = EnvironmentVariable::first();
if (!$env) {
$decrypted = 'No environment variables found to test decryption';
} else {
$encryptedValue = $env->getAttributes()['value'];
try {
Crypt::decryptString($encryptedValue);
$decrypted = 'OK';
} catch (DecryptException $e) {
$decrypted = 'FAILED! ' . $e->getMessage();
}

if (!$hasMismatch) {
$this->info('All configurations match as expected!');
}

return $hasMismatch ? Command::FAILURE : Command::SUCCESS;
}

return Command::SUCCESS;
$other = [
['Landlord Config Cache Path', base_path('bootstrap/cache/config.php')],
['Landlord Config Is Cached', File::exists(base_path('bootstrap/cache/config.php')) ? 'Yes' : 'No'],
['Tenant Config Cache Path', app()->getCachedConfigPath()],
['Tenant Config Is Cached', File::exists(app()->getCachedConfigPath()) ? 'Yes' : 'No'],
['First username (database check)', User::first()?->username ?? 'No users found'],
['Decrypted check', substr($decrypted, 0, 50)],
['Original App URL (landlord)', $currentTenant?->getOriginalValue('APP_URL') ?? config('app.url')],
];

// Display other in a nice table
$this->table(['Other', 'Value'], $other);
}
}
19 changes: 5 additions & 14 deletions ProcessMaker/Exception/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ class Handler extends ExceptionHandler
*/
public function report(Throwable $exception)
{
if (!App::getFacadeRoot()) {
error_log(get_class($exception) . ': ' . $exception->getMessage());

return;
}
if (App::environment() == 'testing' && env('TESTING_VERBOSE')) {
// If we're verbose, we should print ALL Exceptions to the screen
echo $exception->getMessage() . "\n";
Expand Down Expand Up @@ -146,18 +151,4 @@ protected function convertExceptionToArray(Throwable $e)
'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
];
}

/**
* Errors in the console must have an exit status > 0 for CI to see it as an error.
* This prevents the symfony console from handling the error and returning an
* exit status of 0, which it does by default surprisingly.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @param Throwable $e
* @return void
*/
public function renderForConsole($output, Throwable $e)
{
throw $e;
}
}
27 changes: 22 additions & 5 deletions ProcessMaker/Jobs/RefreshArtisanCaches.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
namespace ProcessMaker\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Support\Facades\Artisan;

class RefreshArtisanCaches implements ShouldQueue
class RefreshArtisanCaches implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable;

public $tries = 1;
public $tries = 2; // One extra try to handle the debounce release

public $queuedAt;

/**
* Create a new job instance.
Expand All @@ -22,7 +25,7 @@ class RefreshArtisanCaches implements ShouldQueue
*/
public function __construct()
{
//
$this->queuedAt = time();
}

/**
Expand All @@ -32,7 +35,9 @@ public function __construct()
*/
public function middleware(): array
{
return [(new WithoutOverlapping('refresh_artisan_caches'))->dontRelease()];
return [
(new WithoutOverlapping('refresh_artisan_caches'))->dontRelease(),
];
}

/**
Expand All @@ -49,14 +54,26 @@ public function handle()
return;
}

// Wait 3 seconds before running the job - debounce
if ($this->queuedAt && $this->queuedAt >= time() - 3) {
$this->release(3);

return;
}

$options = [
'--no-interaction' => true,
'--quiet' => true,
];

if (app()->configurationIsCached()) {
Artisan::call('config:cache', $options);
} else {
Artisan::call('queue:restart', $options);

// We call this manually here since this job is dispatched
// automatically when the config *is* cached
RestartMessageConsumers::dispatchSync();
}
Artisan::call('queue:restart', $options);
}
}
20 changes: 4 additions & 16 deletions ProcessMaker/LicensedPackageManifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use ProcessMaker\Providers\ProcessMakerServiceProvider;
use Spatie\Multitenancy\MultitenancyServiceProvider;
use Throwable;

class LicensedPackageManifest extends PackageManifest
Expand All @@ -20,20 +22,6 @@ class LicensedPackageManifest extends PackageManifest

const LAST_PACKAGE_DISCOVERY = 0;

/**
* Consider this the beginning of licenesing refactor for multitenancy.
*
* For now, this will just move the Spatie MultitenancyServiceProvider to the beginning of the service providers.
*/
protected function getManifest()
{
$manifest = parent::getManifest();
$multitenancyKey = 'spatie/laravel-multitenancy';

// Make sure the MultitenancyServiceProvider is at the beginning of the manifest
return [$multitenancyKey => $manifest[$multitenancyKey]] + $manifest;
}

protected function packagesToIgnore()
{
$packagesToIgnore = $this->loadPackagesToIgnore()->all();
Expand Down Expand Up @@ -66,7 +54,7 @@ private function parseLicense()
if (!$this->hasLicenseFile()) {
return null;
}
$license = Storage::disk('root')->get('license.json');
$license = Storage::disk('local')->get('license.json');

return json_decode($license, true);
}
Expand All @@ -87,7 +75,7 @@ private function licensedPackages()

private function hasLicenseFile()
{
return Storage::disk('root')->exists('license.json');
return Storage::disk('local')->exists('license.json');
}

private function setExpireCache()
Expand Down
Loading
Loading