<?php
/*
TODO:
+ Создание метода для получения state скрипта
+ Вывод списков скриптов с полем state
+ Интегрировать поведение относительно поля state при запуске скриптов. Выключенные скрипты не должны быть запущены.
*/
namespace SHServ\Controllers;
use \SHServ\Middleware\ControlScripts;
use \SHServ\Models\Scripts;
use \SHServ\Entities\Script;
use \SHServ\Entities\Area;
use \SHServ\Integrations\GAuth\AuthControllerTrait;
use \SHServ\Helpers\ScriptParamsValidator;
class ScriptsRESTAPIController extends \SHServ\Middleware\Controller {
use AuthControllerTrait;
public function run_action_script($alias, $params) {
if ($auth = $this -> require_permission('scripts.run')) { return $auth; }
if(is_string($params)) {
$decoded = json_decode($params, true);
if(json_last_error() !== JSON_ERROR_NONE) {
logging() -> warn('php:Scripts', 'Run action script failed: invalid JSON params', ['alias' => $alias]);
return $this -> utils() -> response_error("invalid_json", ["params"]);
}
$params = $decoded;
}
if(!is_array($params)) {
logging() -> warn('php:Scripts', 'Run action script failed: invalid params', ['alias' => $alias]);
return $this -> utils() -> response_error("invalid_params", ["params"]);
}
if(!preg_match('/^[a-z0-9_]+$/', $alias) || strlen($alias) > 255) {
logging() -> warn('php:Scripts', 'Run action script failed: invalid alias', ['alias' => $alias]);
return $this -> utils() -> response_error("invalid_alias", ["alias"]);
}
// Валидация параметров по schema
$reg = app() -> scripts_registry;
$schema = $reg -> actions[$alias]["params_schema"] ?? null;
$validation = \SHServ\Helpers\ScriptParamsValidator::validate($schema, $params);
if(!$validation["ok"]) {
$failed_fields = array_column($validation["errors"], "field");
logging() -> warn('php:Scripts', 'Run action script failed: invalid params', ['alias' => $alias, 'errors' => $validation["errors"]]);
return $this -> utils() -> response_error("invalid_params", $failed_fields);
}
$params = $validation["params"];
logging() -> info('php:Scripts', 'Run action script', ['alias' => $alias]);
$result = ControlScripts::run_action_script($alias, $params);
if(!$result) {
logging() -> warn('php:Scripts', 'Run action script failed: not found or disabled', ['alias' => $alias]);
return $this -> utils() -> response_error("action_script_not_found");
}
logging() -> info('php:Scripts', 'Run action script successful', ['alias' => $alias, 'exec_time' => $result['exec_time'] ?? null]);
return $this -> utils() -> response_success([
"return" => $result
]);
}
public function action_script($uniq_name) {
if ($auth = $this -> require_permission('scripts.view')) { return $auth; }
if(!preg_match('/^[a-z0-9_]+$/', $uniq_name) || strlen($uniq_name) > 255) {
logging() -> warn('php:Scripts', 'Action script fetch failed: invalid alias', ['alias' => $uniq_name]);
return $this -> utils() -> response_error("invalid_alias", ["alias"]);
}
$script = (new Scripts()) -> action_script_by_alias($uniq_name);
if(!$script) {
logging() -> warn('php:Scripts', 'Action script fetch failed: not found', ['alias' => $uniq_name]);
return $this -> utils() -> response_error("action_script_not_found");
}
logging() -> trace('php:Scripts', 'Action script fetched', ['alias' => $uniq_name]);
return $this -> utils() -> response_success(["script" => $script]);
}
public function actions_scripts_list() {
if ($auth = $this -> require_permission('scripts.view')) { return $auth; }
$data = (new Scripts()) -> actions_scripts_list();
logging() -> trace('php:Scripts', 'Action scripts list fetched', ['total' => count($data)]);
return $this -> utils() -> response_success([
"scripts" => $data,
"total" => count($data)
]);
}
public function regular_scripts_list() {
if ($auth = $this -> require_permission('scripts.view')) { return $auth; }
$data = (new Scripts()) -> regular_scripts_list();
logging() -> trace('php:Scripts', 'Regular scripts list fetched', ['total' => count($data)]);
return $this -> utils() -> response_success([
"scripts" => $data,
"total" => count($data)
]);
}
public function scope_list() {
if ($auth = $this -> require_permission('scripts.view')) { return $auth; }
$scripts_model = new Scripts();
$scopes = $scripts_model -> get_scopes_list();
logging() -> trace('php:Scripts', 'Scope list fetched', ['total' => count($scopes)]);
return $this -> utils() -> response_success([
"scopes" => $scopes,
"total" => count($scopes)
]);
}
public function scope_file($name) {
if ($auth = $this -> require_permission('scripts.view')) { return $auth; }
$scripts_model = new Scripts();
$scopes = $scripts_model -> get_scopes_list();
$file = "";
foreach($scopes as $scope) {
if($name != $scope["name"]) {
continue;
}
$file = file_get_contents($scope["path"] . "/" . $scope["filename"]);
break;
}
if(!$file) {
return $this -> utils() -> response_error("scope_not_found");
}
return $this -> utils() -> response_success(["source" => $file]);
}
public function scope_remove($name) {
if ($auth = $this -> require_permission('scripts.edit')) { return $auth; }
logging() -> warn('php:Scripts', 'Remove scope', ['scope_name' => $name]);
return (new Scripts()) -> remove_scope($name)
? $this -> utils() -> response_success()
: $this -> utils() -> response_error("undefined_error");
}
protected function set_script_state(String $type, String $uniq_name, String $state) {
if ($auth = $this -> require_permission('scripts.edit')) { return $auth; }
if(!in_array($state, ["enable", "disable"], true)) {
logging() -> warn('php:Scripts', 'Set script state failed: invalid state', ['type' => $type, 'alias' => $uniq_name, 'state' => $state]);
return $this -> utils() -> response_error("invalid_state", ["state"]);
}
if($state == "enable") {
$result = (new Scripts()) -> enable_script($type, $uniq_name);
} else {
$result = (new Scripts()) -> disable_script($type, $uniq_name);
}
if ($result === true) {
logging() -> info('php:Scripts', 'Script state changed', ['type' => $type, 'alias' => $uniq_name, 'state' => $state]);
return $this -> utils() -> response_success();
}
logging() -> warn('php:Scripts', 'Set script state failed', ['type' => $type, 'alias' => $uniq_name, 'state' => $state, 'error' => $result]);
return $this -> utils() -> response_error($result);
}
public function set_scope_state($uniq_name, $state) {
return $this -> set_script_state("scope", $uniq_name, $state);
}
public function set_regular_script_state($uniq_name, $state) {
return $this -> set_script_state("regular", $uniq_name, $state);
}
public function set_action_script_state($uniq_name, $state) {
return $this -> set_script_state("action", $uniq_name, $state);
}
public function place_in_area($target_id, $place_in_area_id) {
if ($auth = $this -> require_permission('scripts.edit')) { return $auth; }
if($target_id != intval($target_id) or intval($target_id) < 1) {
return $this -> utils() -> response_error("invalid_id", ["target_id"]);
}
if($place_in_area_id != intval($place_in_area_id) or intval($place_in_area_id) < 1) {
return $this -> utils() -> response_error("invalid_id", ["place_in_area_id"]);
}
$script = new Script(intval($target_id));
$place_area = new Area(intval($place_in_area_id));
if(!$script) {
logging() -> warn('php:Scripts', 'Place script in area failed: not found', ['script_id' => $target_id]);
return $this -> utils() -> response_error("script_not_exists");
}
if(!$place_area) {
logging() -> warn('php:Scripts', 'Place script in area failed: area not found', ['area_id' => $place_in_area_id]);
return $this -> utils() -> response_error("area_not_exists");
}
logging() -> info('php:Scripts', 'Place script in area', ['script_id' => $script -> id(), 'alias' => $script -> uniq_name, 'area_id' => $place_area -> id()]);
if(!$script -> place_in_area($place_area)) {
logging() -> warn('php:Scripts', 'Place script in area failed', ['script_id' => $script -> id(), 'area_id' => $place_area -> id()]);
return $this -> utils() -> response_error("undefined_error");
}
return $this -> utils() -> response_success();
}
public function scope_update($name, $source) {
if ($auth = $this -> require_permission('scripts.edit')) { return $auth; }
if(!preg_match('/^[A-Za-z0-9_]+$/', $name) || strlen($name) > 255) {
logging() -> warn('php:Scripts', 'Scope update failed: invalid name', ['name' => $name]);
return $this -> utils() -> response_error("invalid_alias", ["name"]);
}
if(!is_string($source) || empty($source)) {
logging() -> warn('php:Scripts', 'Scope update failed: empty source', ['name' => $name]);
return $this -> utils() -> response_error("invalid_source", ["source"]);
}
// Validate syntax via php -l
$tmpFile = tempnam(sys_get_temp_dir(), 'scope_');
file_put_contents($tmpFile, $source);
$output = [];
$returnCode = 0;
exec("php -l " . escapeshellarg($tmpFile) . " 2>&1", $output, $returnCode);
unlink($tmpFile);
if($returnCode !== 0) {
logging() -> warn('php:Scripts', 'Scope update failed: syntax error', ['name' => $name]);
return $this -> utils() -> response_error("syntax_error", ["source"], implode("\n", $output));
}
// Validate namespace
if(strpos($source, 'namespace ControlScripts\\Scopes;') === false) {
logging() -> warn('php:Scripts', 'Scope update failed: invalid namespace', ['name' => $name]);
return $this -> utils() -> response_error("invalid_namespace", ["source"]);
}
// Validate class name
if(strpos($source, "class {$name}") === false) {
logging() -> warn('php:Scripts', 'Scope update failed: class name mismatch', ['name' => $name]);
return $this -> utils() -> response_error("class_name_mismatch", ["source"]);
}
$targetPath = __DIR__ . "/../../../automation/Scopes/{$name}.php";
// Backup existing file
if(file_exists($targetPath)) {
$backupPath = $targetPath . ".bak." . time();
copy($targetPath, $backupPath);
}
if(file_put_contents($targetPath, $source) === false) {
logging() -> error('php:Scripts', 'Scope update failed: write error', ['name' => $name, 'path' => $targetPath]);
return $this -> utils() -> response_error("write_failed");
}
// Invalidate opcache so next request picks up the new file
if(function_exists('opcache_invalidate')) {
opcache_invalidate($targetPath, true);
}
logging() -> info('php:Scripts', 'Scope updated', ['name' => $name, 'path' => $targetPath]);
return $this -> utils() -> response_success();
}
public function timers_list() {
if ($auth = $this -> require_permission('scripts.view')) { return $auth; }
$timers_model = new \SHServ\Models\Timers();
$timers = $timers_model -> get_timers();
logging() -> trace('php:Scripts', 'Timers list fetched', ['total' => count($timers)]);
return $this -> utils() -> response_success([
"timers" => $timers,
"total" => count($timers)
]);
}
public function timers_cancel($timer_alias) {
if ($auth = $this -> require_permission('scripts.edit')) { return $auth; }
if(!preg_match('/^[a-z0-9_]+$/', $timer_alias) || strlen($timer_alias) > 255) {
logging() -> warn('php:Scripts', 'Timer cancel failed: invalid alias', ['timer_alias' => $timer_alias]);
return $this -> utils() -> response_error("invalid_alias", ["timer_alias"]);
}
$timers_model = new \SHServ\Models\Timers();
$result = $timers_model -> cancel_by_alias($timer_alias);
if(!$result) {
logging() -> warn('php:Scripts', 'Timer cancel failed', ['timer_alias' => $timer_alias]);
return $this -> utils() -> response_error("timer_not_found");
}
logging() -> info('php:Scripts', 'Timer cancelled', ['timer_alias' => $timer_alias]);
return $this -> utils() -> response_success();
}
public function unassign_from_area($target_id) {
if ($auth = $this -> require_permission('scripts.edit')) { return $auth; }
if($target_id != intval($target_id) or intval($target_id) < 1) {
return $this -> utils() -> response_error("invalid_id", ["target_id"]);
}
$script = new Script(intval($target_id));
if(!$script) {
logging() -> warn('php:Scripts', 'Unassign script from area failed: not found', ['script_id' => $target_id]);
return $this -> utils() -> response_error("script_not_exists");
}
logging() -> info('php:Scripts', 'Unassign script from area', ['script_id' => $script -> id(), 'alias' => $script -> uniq_name]);
return $script -> place_in_area_id(0)
? $this -> utils() -> response_success()
: $this -> utils() -> response_error("undefined_error");
}
}