Skip to content
Open
35 changes: 35 additions & 0 deletions ProcessMaker/Contracts/ScriptModuleInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace ProcessMaker\Contracts;

use ProcessMaker\ScriptRuntime\ScriptExecutionContext;

/**
* A package-provided module exposed to allow_in_process PHP scripts.
*
* Scripts access the module as ${key}, e.g. $collections->all().
*/
interface ScriptModuleInterface
{
/**
* Unique key used as the script variable name (e.g. "collections").
*/
public static function key(): string;

/**
* Human-readable label for docs / UI catalog.
*/
public static function label(): string;

/**
* Method catalog for discovery (key => meta).
*
* @return array<string, array<string, mixed>>
*/
public static function catalog(): array;

/**
* Prepare the module for a single script execution.
*/
public function boot(ScriptExecutionContext $context): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ interface ServiceTaskImplementationInterface
* @param array $config
* @param string $tokenId
*
* @return string $tokenId
* @return mixed
*
* Implementations may accept an optional 4th argument `$timeout` (seconds).
* WorkflowManager passes it when available (see CoreServiceTask).
*/
public function run(array $data, array $config, $tokenId = '');
}
3 changes: 2 additions & 1 deletion ProcessMaker/Contracts/WorkflowManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,11 @@ public function existsServiceImplementation($implementation);
* @param array $data
* @param array $config
* @param string $tokenId
* @param int|float $timeout
*
* @return mixed
*/
public function runServiceImplementation($implementation, array $data, array $config, $tokenId = '');
public function runServiceImplementation($implementation, array $data, array $config, $tokenId = '', $timeout = 0);

/**
* Get the service task class implementation
Expand Down
24 changes: 24 additions & 0 deletions ProcessMaker/Facades/ScriptRuntime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace ProcessMaker\Facades;

use Illuminate\Support\Facades\Facade;
use ProcessMaker\ScriptRuntime\ScriptExecutionContext;
use ProcessMaker\ScriptRuntime\ScriptModuleRegistry;

/**
* @method static void registerModule(string $moduleClass, ?string $key = null)
* @method static ScriptModuleRegistry registry()
* @method static array catalog()
* @method static mixed run(string $code, ScriptExecutionContext $context)
* @method static array normalizeOutput(mixed $output)
*
* @see \ProcessMaker\ScriptRuntime\ScriptRuntime
*/
class ScriptRuntime extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'script.runtime';
}
}
2 changes: 1 addition & 1 deletion ProcessMaker/Facades/WorkflowManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* @method static array runProcess(Process $process, $startEventId, array $data)
* @method static bool registerServiceImplementation($implementation, $class)
* @method static bool existsServiceImplementation($implementation)
* @method static bool runServiceImplementation($implementation, array $data, array $config, $tokenId = '')
* @method static mixed runServiceImplementation($implementation, array $data, array $config, $tokenId = '', $timeout = 0)
*/
class WorkflowManager extends Facade
{
Expand Down
30 changes: 28 additions & 2 deletions ProcessMaker/Http/Controllers/Api/ScriptController.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ public function index(Request $request)
* property="nonce",
* type="string",
* ),
* @OA\Property(
* property="run_in_process",
* type="boolean",
* description="When true (and Core Service Task enabled), preview via in-process PHP runner instead of Docker/microservice. Also auto-enabled when the script has allow_in_process=true.",
* ),
* ),
* ),
*
Expand All @@ -189,10 +194,31 @@ public function preview(Request $request, Script $script)
$config = $this->getRequestArray($request->get('config'));
$code = $request->get('code');
$nonce = $request->get('nonce');
$runInProcess = $this->shouldPreviewInProcess($request, $script);

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce, $runInProcess)->onQueue('bpmn');

return ['status' => 'success', 'run_in_process' => $runInProcess];
}

/**
* Decide if script preview should use Core Service Task in-process runner.
*/
private function shouldPreviewInProcess(Request $request, Script $script): bool
{
if (!config('core-service-task.enabled')) {
return false;
}

TestScript::dispatch($script, $request->user(), $code, $data, $config, $nonce)->onQueue('bpmn');
if (strtolower((string) $script->language) !== 'php') {
return false;
}

if ($request->boolean('run_in_process')) {
return true;
}

return ['status' => 'success'];
return (bool) $script->allow_in_process;
}

/**
Expand Down
83 changes: 81 additions & 2 deletions ProcessMaker/Jobs/TestScript.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
use Illuminate\Queue\SerializesModels;
use ProcessMaker\Enums\ScriptExecutorType;
use ProcessMaker\Events\ScriptResponseEvent;
use ProcessMaker\Exception\ConfigurationException;
use ProcessMaker\Facades\ScriptRuntime;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\User;
use ProcessMaker\ScriptRunners\InProcessPhpRunner;
use ProcessMaker\ScriptRuntime\ScriptExecutionContext;
use Throwable;

class TestScript implements ShouldQueue
Expand All @@ -32,6 +36,8 @@ class TestScript implements ShouldQueue

protected $nonce;

protected bool $runInProcess;

/**
* Create a new job instance to execute a script.
*
Expand All @@ -40,15 +46,25 @@ class TestScript implements ShouldQueue
* @param string $code
* @param array $data
* @param array $configuration
* @param string|null $nonce
* @param bool $runInProcess Use Core Service Task in-process runner (no Docker)
*/
public function __construct(Script $script, User $current_user, $code, array $data, array $configuration, $nonce = null)
{
public function __construct(
Script $script,
User $current_user,
$code,
array $data,
array $configuration,
$nonce = null,
bool $runInProcess = false
) {
$this->script = $script;
$this->current_user = $current_user;
$this->code = $code;
$this->data = $data;
$this->configuration = $configuration;
$this->nonce = $nonce;
$this->runInProcess = $runInProcess;
}

/**
Expand All @@ -61,6 +77,15 @@ public function handle()
try {
// Just set the code but do not save the object (preview only)
$this->script->code = $this->code;

if ($this->runInProcess) {
$response = $this->runInProcessPreview();
\Log::debug('Response from in-process preview: ' . print_r($response, true));
$this->sendResponse(200, $response);

return;
}

$metadata = [
'nonce' => $this->nonce,
'current_user' => $this->current_user?->id,
Expand All @@ -80,6 +105,60 @@ public function handle()
}
}

/**
* Preview using ScriptRuntime modules (default) or bare InProcessPhpRunner.
*
* @return array{output: mixed}
*/
private function runInProcessPreview(): array
{
if (!config('core-service-task.enabled')) {
throw new ConfigurationException(
'Core Service Task is disabled. Set CORE_SERVICE_TASK_ENABLED=true to enable.'
);
}

if (strtolower((string) $this->script->language) !== 'php') {
throw new ConfigurationException('In-process preview only supports PHP scripts');
}

$user = User::find($this->script->run_as_user_id);
if (!$user) {
throw new ConfigurationException('A user is required to run scripts');
}

$timeout = (int) ($this->script->timeout ?: config('core-service-task.default_timeout', 60));
$max = (int) config('core-service-task.max_timeout', 300);
if ($max > 0 && $timeout > $max) {
$timeout = $max;
}
$timeout = max(1, $timeout);

if (config('core-service-task.execution', 'modules') === 'modules') {
$context = new ScriptExecutionContext(
data: $this->data,
config: $this->configuration,
user: $user,
tokenId: '',
timeout: $timeout,
scriptId: (int) $this->script->id,
source: 'preview',
);
$output = ScriptRuntime::run($this->code, $context);
} else {
$output = app(InProcessPhpRunner::class)->run(
$this->code,
$this->data,
$this->configuration,
$timeout,
$user
);
}

// Match runScript() response shape expected by the Script Editor UI
return ['output' => $output];
}

/**
* Send a response to the user interface
*
Expand Down
10 changes: 10 additions & 0 deletions ProcessMaker/Models/Script.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* @property string language
* @property text code
* @property int timeout
* @property bool allow_in_process
*
* @OA\Schema(
* schema="scriptsEditable",
Expand All @@ -38,6 +39,7 @@
* @OA\Property(property="language", type="string"),
* @OA\Property(property="code", type="string"),
* @OA\Property(property="timeout", type="integer"),
* @OA\Property(property="allow_in_process", type="boolean"),
* @OA\Property(property="run_as_user_id", type="integer"),
* @OA\Property(property="key", type="string"),
* @OA\Property(property="script_category_id", type="integer"),
Expand Down Expand Up @@ -87,6 +89,7 @@ class Script extends ProcessMakerModel implements ScriptInterface
'timeout' => 'integer',
'retry_attempts' => 'integer',
'retry_wait_time' => 'integer',
'allow_in_process' => 'boolean',
];

protected $appends = [
Expand All @@ -111,6 +114,12 @@ public static function boot()
// automatically based on the scripts set language
$script->setDefaultExecutor();

if ($script->allow_in_process && strtolower((string) $script->language) !== 'php') {
throw \Illuminate\Validation\ValidationException::withMessages([
'allow_in_process' => [__('Only PHP scripts can allow in-process execution.')],
]);
}

// Execute the clear cache callback
$clearCacheCallback($script);
});
Expand Down Expand Up @@ -154,6 +163,7 @@ public static function rules($existing = null)
'description' => 'required',
'run_as_user_id' => 'required',
'timeout' => 'integer|min:0|max:65535',
'allow_in_process' => 'sometimes|boolean',
'script_category_id' => [new CategoryRule($existing)],
];
}
Expand Down
22 changes: 22 additions & 0 deletions ProcessMaker/Providers/WorkflowServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
use ProcessMaker\Nayra\MessageBrokers\ServiceInterface;
use ProcessMaker\Repositories\BpmnDocument;
use ProcessMaker\Repositories\DefinitionsRepository;
use ProcessMaker\ScriptRuntime\ScriptModuleRegistry;
use ProcessMaker\ScriptRuntime\ScriptRuntime;
use ProcessMaker\ServiceTaskImplementations\CoreServiceTask;
use ProcessMaker\WebServices\Contracts\SoapClientInterface;
use ProcessMaker\WebServices\NativeSoapClient;
use ProcessMaker\WebServices\SoapConfigBuilder;
Expand Down Expand Up @@ -235,6 +238,25 @@ function (ThrowEventInterface $source, EventDefinitionInterface $sourceEventDefi
return ServiceFactory::create();
});

$this->app->singleton(ScriptModuleRegistry::class);
$this->app->singleton(ScriptRuntime::class, function ($app) {
return new ScriptRuntime($app->make(ScriptModuleRegistry::class));
});
$this->app->alias(ScriptRuntime::class, 'script.runtime');

parent::register();
}

/**
* Bootstrap workflow services.
*/
public function boot(): void
{
parent::boot();

WorkflowManagerFacade::registerServiceImplementation(
CoreServiceTask::IMPLEMENTATION,
CoreServiceTask::class
);
}
}
Loading
Loading