diff --git a/ProcessMaker/Application.php b/ProcessMaker/Application.php
index 5cea34ac9a..f9edc37e11 100644
--- a/ProcessMaker/Application.php
+++ b/ProcessMaker/Application.php
@@ -15,7 +15,6 @@
use Illuminate\Support\Facades\Config;
use ProcessMaker\Console\Kernel;
use ProcessMaker\Multitenancy\Tenant;
-use ProcessMaker\Multitenancy\TenantBootstrapper;
/**
* Class Application.
@@ -103,15 +102,4 @@ 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);
- }
}
diff --git a/ProcessMaker/Console/Commands/HorizonListen.php b/ProcessMaker/Console/Commands/HorizonListen.php
deleted file mode 100644
index a3f3dd47b1..0000000000
--- a/ProcessMaker/Console/Commands/HorizonListen.php
+++ /dev/null
@@ -1,51 +0,0 @@
-info('Multitenancy is disabled');
+
+ return;
+ }
+
+ $errors = [];
$currentTenant = null;
if (app()->has('currentTenant')) {
$currentTenant = app('currentTenant');
}
- if (config('app.multitenancy') && !$currentTenant) {
+ if (!$currentTenant) {
$this->error('Multitenancy enabled but no current tenant found.');
return;
}
- $this->info('Current Tenant ID: ' . ($currentTenant?->id ?? 'NONE'));
+ \Log::warning('TenantsVerify: Current Tenant ID: ' . ($currentTenant?->id ?? 'NONE'));
$paths = [
['Storage Path', storage_path()],
@@ -55,32 +64,44 @@ public function handle()
];
// Display paths in a nice table
- $this->table(['Path', 'Value'], $paths);
+ $this->infoTable(['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',
+ 'app.key' => null,
+ 'app.url' => null,
+ 'app.instance' => null,
+ 'cache.prefix' => 'tenant_{tenant_id}:',
+ 'database.redis.options.prefix' => null,
+ 'cache.stores.cache_settings.prefix' => 'tenant_{tenant_id}:settings',
+ 'script-runner-microservice.callback' => null,
+ 'database.connections.processmaker.database' => null,
+ 'logging.channels.daily.path' => base_path() . '/storage/tenant_{tenant_id}/logs/processmaker.log',
+ 'filesystems.disks.public.root' => base_path() . '/storage/tenant_{tenant_id}/app/public',
+ 'filesystems.disks.local.root' => base_path() . '/storage/tenant_{tenant_id}/app',
+ 'filesystems.disks.lang.root' => base_path() . '/resources/lang/tenant_{tenant_id}',
];
- $configs = array_map(function ($config) {
+ $configs = array_map(function ($config) use ($configs, $currentTenant, &$errors) {
+ $ok = '';
+ if ($configs[$config] !== null) {
+ $expected = str_replace('{tenant_id}', $currentTenant->id, $configs[$config]);
+ if (config($config) === $expected) {
+ $ok = '✓';
+ } else {
+ $ok = '✗';
+ $errors[] = 'Expected: ' . $expected . ' != Actual: ' . config($config);
+ }
+ }
+
return [
$config,
config($config),
+ $ok,
];
- }, $configs);
+ }, array_keys($configs));
// Display configs in a nice table
- $this->table(['Config', 'Value'], $configs);
+ $this->infoTable(['Config', 'Value', 'OK'], $configs);
$env = EnvironmentVariable::first();
if (!$env) {
@@ -102,10 +123,62 @@ public function handle()
['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')],
+ // ['Original App URL (landlord)', $currentTenant?->getOriginalValue('APP_URL') ?? config('app.url')],
+ ['config("app.url")', config('app.url')],
+ ['getenv("APP_URL")', getenv('APP_URL')],
+ ['env("APP_URL")', env('APP_URL')],
+ ['$_SERVER["APP_URL"]', $_SERVER['APP_URL'] ?? 'NOT SET'],
+ ['$_ENV["APP_URL"]', $_ENV['APP_URL'] ?? 'NOT SET'],
+ ['Current PID', getmypid()],
];
// Display other in a nice table
- $this->table(['Other', 'Value'], $other);
+ $this->infoTable(['Other', 'Value'], $other);
+
+ $checkUrls = [
+ 'config("app.url")' => config('app.url'),
+ 'getenv("APP_URL")' => getenv('APP_URL'),
+ 'env("APP_URL")' => env('APP_URL'),
+ '$_SERVER["APP_URL"]' => $_SERVER['APP_URL'] ?? 'NOT SET',
+ '$_ENV["APP_URL"]' => $_ENV['APP_URL'] ?? 'NOT SET',
+ ];
+
+ foreach ($checkUrls as $key => $value) {
+ if ($value !== $currentTenant?->config['app.url']) {
+ $errors[] = 'Expected: ' . $key . ' to be ' . $currentTenant?->config['app.url'] . ' but got ' . $value;
+ }
+ }
+
+ $this->finish($errors);
+ }
+
+ private function finish($errors)
+ {
+ if (count($errors) > 0) {
+ $this->error('Errors found');
+ } else {
+ $this->info('No errors found');
+ }
+
+ if ($this->option('json')) {
+ $this->jsonData['Errors'] = $errors;
+ $this->line(json_encode($this->jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+ }
+ }
+
+ private function infoTable($headers, $rows)
+ {
+ if ($this->option('json')) {
+ $section = [];
+ foreach ($rows as $row) {
+ $section[$row[0]] = $row[1];
+ }
+ $this->jsonData[$headers[0]] = $section;
+ } else {
+ foreach ($rows as $row) {
+ \Log::warning($row[0] . ': ' . $row[1]);
+ }
+ $this->table($headers, $rows);
+ }
}
}
diff --git a/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php b/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php
index 25118798fd..a66ef5d9da 100644
--- a/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php
+++ b/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php
@@ -7,11 +7,14 @@
class MakeQueueTenantAwareAction extends BaseMakeQueueTenantAwareAction
{
/**
- * We're handling tenant aware queues manually, however, we still need to implement this because for some
- * reason the Spatie package calls it in Multitenancy::start(), weather it's a configured action or not.
+ * Non-multitenant environments shouldn't throw an exception if the tenant is not found.
*/
public function execute() : void
{
- // Do nothing
+ if (!config('app.multitenancy')) {
+ return;
+ }
+
+ parent::execute();
}
}
diff --git a/ProcessMaker/Multitenancy/PrefixCacheTask.php b/ProcessMaker/Multitenancy/PrefixCacheTask.php
new file mode 100644
index 0000000000..30aa0c127a
--- /dev/null
+++ b/ProcessMaker/Multitenancy/PrefixCacheTask.php
@@ -0,0 +1,32 @@
+getKey() . ':';
+ $this->setCachePrefix($cachePrefix);
+
+ $this->originalSettingsPrefix = config('cache.stores.cache_settings.prefix');
+ $tenantSettingsPrefix = 'tenant_' . $tenant->getKey() . ':' . $this->originalSettingsPrefix;
+ config()->set('cache.stores.cache_settings.prefix', $tenantSettingsPrefix);
+ $this->storeName = 'cache_settings';
+ $this->setCachePrefix($cachePrefix);
+ }
+
+ public function forgetCurrent(): void
+ {
+ $this->setCachePrefix($this->originalPrefix);
+
+ config()->set('cache.stores.cache_settings.prefix', $this->originalSettingsPrefix);
+ $this->storeName = 'cache_settings';
+ $this->setCachePrefix($this->originalPrefix);
+ }
+}
diff --git a/ProcessMaker/Multitenancy/SwitchTenant.php b/ProcessMaker/Multitenancy/SwitchTenant.php
index eb5e319881..d6ea262135 100644
--- a/ProcessMaker/Multitenancy/SwitchTenant.php
+++ b/ProcessMaker/Multitenancy/SwitchTenant.php
@@ -3,6 +3,9 @@
namespace ProcessMaker\Multitenancy;
use Illuminate\Broadcasting\BroadcastManager;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Env;
+use Monolog\Handler\RotatingFileHandler;
use ProcessMaker\Application;
use ProcessMaker\Multitenancy\Broadcasting\TenantAwareBroadcastManager;
use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig;
@@ -13,6 +16,8 @@ class SwitchTenant implements SwitchTenantTask
{
use UsesMultitenancyConfig;
+ public static $landlordValues = null;
+
/**
* Make the given tenant current.
*
@@ -25,6 +30,11 @@ public function makeCurrent(IsTenant $tenant): void
\Log::debug('SwitchTenant: ' . $tenant->id, ['domain' => request()->getHost()]);
+ // Save the landlord values for later use
+ if (!self::$landlordValues) {
+ self::$landlordValues = $app->make('config')->all();
+ }
+
// Set the tenant's domain in the request headers. Used for things like the global url() helper.
request()->headers->set('host', $tenant->domain);
@@ -43,29 +53,86 @@ public function makeCurrent(IsTenant $tenant): void
*/
public function forgetCurrent(): void
{
+ $app = app();
+ $app->useStoragePath(base_path('storage'));
+
+ $this->setConfig('logging.channels.daily.path', storage_path('logs/processmaker.log'));
+ $app->make('log')->reset();
+ $app->forgetInstance('log');
+
+ $app->useLangPath(resource_path('lang'));
+
+ // app key / encrypter
+ $this->setConfig('app.key', $this->landlordConfig('app.key'));
+ $app->forgetInstance('encrypter');
+ }
+
+ private function landlordConfig($key)
+ {
+ return Arr::get(self::$landlordValues, $key);
+ }
+
+ private function setConfig($key, $value)
+ {
+ app()->make('config')->set($key, $value);
+ }
+
+ private function setEnvironmentVariable($key, $value)
+ {
+ putenv("$key=$value");
+ $_SERVER[$key] = $value;
+ $_ENV[$key] = $value;
}
private function overrideConfigs(Application $app, IsTenant $tenant)
{
- if ($app->configurationIsCached()) {
- return;
- }
+ $this->setEnvironmentVariable('APP_URL', $tenant->config['app.url']);
- $newConfig = [
- 'app.instance' => config('app.instance') . '_' . $tenant->id,
- ];
+ $this->setConfig('app.instance', $this->landlordConfig('app.instance') . '_' . $tenant->id);
+ $this->setConfig('app.url', $tenant->config['app.url']);
+ // Microservice callback url
if (!isset($tenant->config['script-runner-microservice.callback'])) {
- $newConfig['script-runner-microservice.callback'] = str_replace(
- $tenant->getOriginalValue('APP_URL'),
- config('app.url'),
- $tenant->getOriginalValue('SCRIPT_MICROSERVICE_CALLBACK')
- );
+ $this->setConfig('script-runner-microservice.callback', str_replace(
+ $this->landlordConfig('app.url'),
+ $tenant->config['app.url'],
+ $this->landlordConfig('script-runner-microservice.callback')
+ ));
+ }
+
+ // Filesystem roots
+ $landlordStoragePath = base_path('storage');
+ $newStoragePath = base_path('storage/tenant_' . $tenant->id);
+ foreach ($this->landlordConfig('filesystems.disks') as $disk => $config) {
+ if (isset($config['root'])) {
+ $this->setConfig('filesystems.disks.' . $disk . '.root', str_replace(
+ $landlordStoragePath,
+ $newStoragePath,
+ $config['root']
+ ));
+ }
}
+ $app->useStoragePath($newStoragePath);
+
+ // Lang path
+ $app->useLangPath(resource_path('lang/tenant_' . $tenant->id));
+ $this->setConfig('filesystems.disks.lang.root', lang_path());
+
+ // app key / encrypter
+ $landlordEncrypter = $app->make('encrypter');
+ $this->setConfig('app.key', $landlordEncrypter->decryptString($tenant->config['app.key']));
+ $app->forgetInstance('encrypter');
+
+ // Logging
+ $this->setConfig('logging.channels.daily.path', storage_path('logs/processmaker.log'));
+ $app->make('log')->reset();
+ $app->forgetInstance('log');
+
+ // NOTE: Cache prefix and cache settings prefix are handled in PrefixCacheTask
if (!isset($tenant->config['app.docker_host_url'])) {
// There is no specific override in the tenant's config so set it to the app url
- $newConfig['app.docker_host_url'] = config('app.url');
+ $this->setConfig('app.docker_host_url', $tenant->config['app.url']);
}
// Set config from the entry in the tenants table
@@ -74,9 +141,7 @@ private function overrideConfigs(Application $app, IsTenant $tenant)
if ($key === 'app.key' || $key === 'app.url') {
continue;
}
- $newConfig[$key] = $value;
+ $this->setConfig($key, $value);
}
-
- config($newConfig);
}
}
diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php
deleted file mode 100644
index 7ab897f0e8..0000000000
--- a/ProcessMaker/Multitenancy/TenantBootstrapper.php
+++ /dev/null
@@ -1,189 +0,0 @@
-bootstrapRun($app);
- } catch (\Exception $e) {
- file_put_contents(storage_path('logs/tenant_bootstrapper_error.log'), date('Y-m-d H:i:s') . ' ' . get_class($e) . ' in ' . $e->getFile() . ':' . $e->getLine() . ' ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
- throw $e;
- }
- }
-
- public function bootstrapRun(Application $app)
- {
- if (!$this->env('MULTITENANCY')) {
- return;
- }
- $this->app = $app;
-
- $tenantData = null;
-
- // Try to find tenant by ID first if TENANT env var is set
- $envTenant = $this->env('TENANT');
- if ($envTenant) {
- $tenantData = $this->executeQuery('SELECT * FROM tenants WHERE id = ?', [$envTenant]);
- } else {
- $request = Request::capture();
- $host = $request->getHost();
- $tenantData = $this->executeQuery('SELECT * FROM tenants WHERE domain = ? LIMIT 1', [$host]);
- }
-
- if (!$tenantData) {
- Tenant::setBootstrappedTenant($app, null);
-
- return;
- }
-
- // Set storage path
- $app->useStoragePath($app->basePath('storage/tenant_' . $tenantData['id']));
-
- $this->setTenantEnvironmentVariables($tenantData);
-
- // Use tenant's translation files. Doing this here so it's available in cached filesystems.php
- $app->useLangPath(resource_path('lang/tenant_' . $tenantData['id']));
-
- $tenantData['original_values'] = self::$landlordValues;
- Tenant::setBootstrappedTenant($app, $tenantData);
- }
-
- private function setTenantEnvironmentVariables($tenantData)
- {
- // Additional configs are set in SwitchTenant.php
-
- $config = json_decode($tenantData['config'], true);
-
- $this->set('APP_CONFIG_CACHE', $this->app->storagePath('config.php'));
- $this->set('APP_URL', $config['app.url']);
- $this->set('APP_KEY', $this->decrypt($config['app.key']));
- $this->set('DB_DATABASE', $tenantData['database']);
- $this->set('DB_USERNAME', $tenantData['username'] ?? $this->getOriginalValue('DB_USERNAME'));
-
- // Do not set REDIS_PREFIX because it is used by the queue (not tenant specific)
- $this->set('CACHE_PREFIX', 'tenant_' . $tenantData['id'] . ':' . $this->getOriginalValue('CACHE_PREFIX'));
- $this->set('CACHE_SETTING_PREFIX', 'tenant_' . $tenantData['id'] . ':' . $this->getOriginalValue('CACHE_SETTING_PREFIX'));
-
- $encryptedPassword = $tenantData['password'];
- $password = null;
- if ($encryptedPassword) {
- $password = $this->decrypt($encryptedPassword);
- } else {
- $password = $this->getOriginalValue('DB_PASSWORD');
- }
-
- $this->set('DB_PASSWORD', $password);
- $this->set('LOG_PATH', $this->app->storagePath('logs/processmaker.log'));
- }
-
- // Used for debugging
- public static function log($message)
- {
- $message = '[TenantBootstrapper] ' . $message;
- $today = date('Y-m-d');
- file_put_contents(base_path('storage/logs/processmaker-' . $today . '.log'), date('Y-m-d H:i:s') . ' ' . $message . PHP_EOL, FILE_APPEND);
- }
-
- private function getOriginalValue($key, $default = '')
- {
- if (self::$landlordValues === []) {
- self::$landlordValues = Dotenv::parse(file_get_contents(base_path('.env')));
- }
-
- if (!isset(self::$landlordValues[$key])) {
- return $default;
- }
-
- return self::$landlordValues[$key];
- }
-
- private function env($key, $default = null)
- {
- $value = $_SERVER[$key] ?? $default;
- if ($value === 'true') {
- $value = true;
- } elseif ($value === 'false') {
- $value = false;
- }
-
- return $value;
- }
-
- private function set($key, $value)
- {
- // Env::getRepository() is immutable but will use values from $_SERVER and $_ENV
- $_SERVER[$key] = $value;
- $_ENV[$key] = $value;
- putenv("$key=$value");
- }
-
- private function decrypt($value)
- {
- if (!$this->encrypter) {
- $key = $this->getOriginalValue('APP_KEY');
- $landlordKey = base64_decode(substr($key, 7));
- $this->encrypter = new Encrypter($landlordKey, 'AES-256-CBC');
- }
-
- return $this->encrypter->decryptString($value);
- }
-
- private function getLandlordDbConfig(): array
- {
- return [
- 'host' => $this->getOriginalValue('DB_HOSTNAME', 'localhost'),
- 'port' => $this->getOriginalValue('DB_PORT', '3306'),
- 'database' => $this->getOriginalValue('LANDLORD_DB_DATABASE', 'landlord'),
- 'username' => $this->getOriginalValue('DB_USERNAME'),
- 'password' => $this->getOriginalValue('DB_PASSWORD'),
- 'charset' => 'utf8mb4',
- ];
- }
-
- private function getPdo(): PDO
- {
- if (!$this->pdo) {
- $landlordConfig = $this->getLandlordDbConfig();
- $dsn = "mysql:host={$landlordConfig['host']};port={$landlordConfig['port']};dbname={$landlordConfig['database']};charset={$landlordConfig['charset']}";
- $this->pdo = new PDO($dsn, $landlordConfig['username'], $landlordConfig['password'], [
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
- ]);
- }
-
- return $this->pdo;
- }
-
- private function executeQuery($query, $params = [])
- {
- $stmt = $this->getPdo()->prepare($query);
- $stmt->execute($params);
-
- return $stmt->fetch();
- }
-}
diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php
index 92d4569127..1ae630b702 100644
--- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php
+++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php
@@ -46,7 +46,6 @@
use ProcessMaker\Managers\ScreenCompiledManager;
use ProcessMaker\Models;
use ProcessMaker\Multitenancy\Tenant;
-use ProcessMaker\Multitenancy\TenantBootstrapper;
use ProcessMaker\Observers;
use ProcessMaker\PolicyExtension;
use ProcessMaker\Providers\PermissionServiceProvider;
@@ -250,40 +249,6 @@ public function register(): void
* This service is used to evaluate the conditional redirect property of a process request token.
*/
$this->app->bind(ConditionalRedirectServiceInterface::class, ConditionalRedirectService::class);
-
- $this->app->when(HorizonListen::class)->needs(Listener::class)->give(function ($app) {
- return new Listener(base_path());
- });
-
- if (config('app.multitenancy')) {
- WorkerCommandString::$command = 'exec @php artisan horizon:listen';
- SystemProcessCounter::$command = 'horizon:listen';
- }
- }
-
- /**
- * In multitenancy, we need to bootstrap a new app with the tenant id set.
- * This is because queue workers are long-running processes that are not
- * tenant aware.
- */
- private static function bootstrapTenantApp(JobProcessing|JobRetryRequested $event): void
- {
- Context::hydrate($event->job->payload()['illuminate:log:context'] ?? null);
- $tenantId = Context::get(config('multitenancy.current_tenant_context_key'));
- if ($tenantId) {
- if (!method_exists($event->job, 'getRedisQueue')) {
- // Not a redis job
- return;
- }
-
- // Create a new tenant app instance
- $_SERVER['TENANT'] = $tenantId;
-
- $app = require base_path('bootstrap/app.php');
- $app->make(\ProcessMaker\Console\Kernel::class)->bootstrap();
- \Illuminate\Container\Container::setInstance($app);
- $event->job->getRedisQueue()->setContainer($app);
- }
}
/**
@@ -291,14 +256,6 @@ private static function bootstrapTenantApp(JobProcessing|JobRetryRequested $even
*/
protected static function registerEvents(): void
{
- Facades\Event::listen(JobProcessing::class, function (JobProcessing $event) {
- self::bootstrapTenantApp($event);
- });
-
- Facades\Event::listen(JobRetryRequested::class, function (JobRetryRequested $event) {
- self::bootstrapTenantApp($event);
- });
-
// Listen to the events for our core screen
// types and add our javascript
Facades\Event::listen(ScreenBuilderStarting::class, function ($event) {
diff --git a/config/multitenancy.php b/config/multitenancy.php
index 7c56e74d8f..8f69499381 100644
--- a/config/multitenancy.php
+++ b/config/multitenancy.php
@@ -1,10 +1,6 @@
[
SwitchTenant::class,
+ ProcessMaker\Multitenancy\PrefixCacheTask::class,
+ Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class,
],
/*
@@ -86,7 +84,7 @@
'actions' => [
'make_tenant_current_action' => MakeTenantCurrentAction::class,
'forget_current_tenant_action' => ForgetCurrentTenantAction::class,
- 'make_queue_tenant_aware_action' => ProcessMaker\Multitenancy\MakeQueueTenantAwareAction::class,
+ 'make_queue_tenant_aware_action' => MakeQueueTenantAwareAction::class,
'migrate_tenant' => MigrateTenantAction::class,
],
diff --git a/phpunit.xml b/phpunit.xml
index 457f6737a1..81690f715b 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -78,5 +78,7 @@
+
+