api.configforge.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. <?php
  2. /**
  3. * ConfigForge class for managing configuration files
  4. *
  5. * This class provides functionality to load, edit, and save configuration files
  6. * based on a specification file.
  7. * available types: TEXT, CHECKBOX, RADIO, SELECT, TRIGGER, PASSWORD, SLIDER
  8. * available patterns for TEXT type:
  9. * - alpha: only Latin letters [a-zA-Z] (e.g., "abcDEF")
  10. * - alphanumeric: only Latin letters and numbers [a-zA-Z0-9] (e.g., "abc123")
  11. * - digits: only digits [0-9] (e.g., "12345")
  12. * - email: valid email address format (e.g., "user@domain.com")
  13. * - finance: decimal numbers with optional decimal point (e.g., "123.45", "100", "0.99")
  14. * - float: floating point numbers (e.g., "123.45", "0.001")
  15. * - fullpath: absolute Unix-style paths starting with / (e.g., "/var/www/html")
  16. * - geo: geographic coordinates (e.g., "40.7143528,-74.0059731")
  17. * - ip: IPv4 address format (e.g., "192.168.1.1")
  18. * - login: username format with letters, numbers and underscore (e.g., "user_123")
  19. * - mac: MAC address format with : or - separator (e.g., "00:1A:2B:3C:4D:5E")
  20. * - mobile: phone number with optional country code (e.g., "+380501234567")
  21. * - net-cidr: network CIDR notation, mask can't be /31 (e.g., "192.168.1.0/24")
  22. * - path: relative or absolute Unix-style paths (e.g., "dir/file.txt", "/etc/config")
  23. * - pathorurl: URLs with optional ports or paths (e.g., "http://example.com:8080", "some/dir/")
  24. * - sigint: signed integers (e.g., "-123", "456")
  25. * - url: HTTP/HTTPS URLs with optional port numbers (e.g., "http://example.com:8080")
  26. *
  27. * Specification file example:
  28. *
  29. * [sectionname]
  30. * LABEL="Option label"
  31. * OPTION=SOME_OPTION
  32. * TYPE=CHECKBOX
  33. * DEFAULT=0
  34. *
  35. * [sectionname2]
  36. * LABEL="Your sex?"
  37. * OPTION=SEX
  38. * TYPE=SELECT
  39. * VALUES="male,female,unknown"
  40. * DEFAULT="unknown"
  41. * SAVEFILTER="gigasafe"
  42. *
  43. * [sectionname3]
  44. * LABEL="Option label 2"
  45. * OPTION=ANOTHER_OPTION
  46. * TYPE=TEXT
  47. * PATTERN="mac"
  48. * VALIDATOR="IsMacValid"
  49. * ONINVALID="This mac address is invalid"
  50. * DEFAULT="14:88:92:94:94:61"
  51. *
  52. * [sectionname4]
  53. * LABEL="Volume level"
  54. * OPTION=VOLUME
  55. * VALUES="0..100"
  56. * TYPE=SLIDER
  57. * DEFAULT=50
  58. *
  59. *
  60. * class usage example:
  61. * $configPath = 'config/test.ini';
  62. * $specPath = 'config/test.spec';
  63. * $forge = new ConfigForge($configPath, $specPath);
  64. * $processResult = $forge->process();
  65. * if (!empty($processResult)) {
  66. * show_error($processResult);
  67. * } elseif (ubRouting::post(ConfigForge::FORM_SUBMIT_KEY)==$forge->getInstanceId()) {
  68. * ubRouting::nav('?module=testing');
  69. * }
  70. *
  71. * show_window(__('Config Forge'), $forge->renderEditor());
  72. */
  73. class ConfigForge {
  74. /**
  75. * Contains current config lines as index=>line
  76. *
  77. * @var array
  78. */
  79. protected $currentConfig = array();
  80. /**
  81. * Path to the config file
  82. *
  83. * @var string
  84. */
  85. protected $configPath = '';
  86. /**
  87. * Contains parsed config data as section=>key=>value
  88. *
  89. * @var array
  90. */
  91. protected $parsedConfig = array();
  92. /**
  93. * Contains comments for each config line
  94. *
  95. * @var array
  96. */
  97. protected $lineComments = array();
  98. /**
  99. * Path to the spec file
  100. *
  101. * @var string
  102. */
  103. protected $specPath = '';
  104. /**
  105. * Unique identifier for this instance
  106. *
  107. * @var string
  108. */
  109. protected $instanceId = '';
  110. /**
  111. * Form CSS class
  112. *
  113. * @var string
  114. */
  115. protected $formClass = 'glamour';
  116. /**
  117. * Form submission identifier
  118. */
  119. const FORM_SUBMIT_KEY = 'configforge_submit';
  120. // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠀⠀⠀⠀⠀⠀
  121. // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⣾⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⣴⡏⠀⠀⢀⠀⠀⠀
  122. // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣷⣄⠀⠀⠀⠀⠀⢀⣼⣿⁣⣤⡶⠁⠀⠀⠀
  123. // ⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣶⣿⣿⣿⣿⣿⡄⠀⢠⣷⣾⣿⣿⣿⣿⣁⣀⡀⠀⠀
  124. // ⠀⠀⠀⢀⣠⣴⣾⡿⠟⠋⠉⠀⠈⢿⣿⣿⠷⠀⣾⠿⠛⣿⣿⠿⠛⠋⠁⠀⠀⠀
  125. // ⠀⢲⣿⡿⠟⠋⠁⠀⠀⠀⠀⢀⣀⣈⣁⣀⣀⣀⣀⣀⣀⣁⣀⣀⣀⡀⠀⠀⠀⠀
  126. // ⠀⠈⠁⠀⠀⠀⢠⣤⣤⣤⣤⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀
  127. // ⠀⠀⠀⠀⠀⠀⠀⠙⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠄⠀
  128. // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠋⠁⠀⠀
  129. // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⢛⣿⣿⣿⣿⣿⣿⡟⠛⠛⠛⠃⠀⠀⠀⠀
  130. // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀
  131. // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⣀⡀⠀⠀⠀
  132. // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⠀
  133. /**
  134. * Creates new ConfigForge instance
  135. *
  136. * @param string $configPath Path to config file
  137. * @param string $specPath Path to spec file
  138. * @return void
  139. */
  140. public function __construct($configPath, $specPath) {
  141. $this->configPath = $configPath;
  142. $this->specPath = $specPath;
  143. $this->instanceId = md5($configPath . $specPath);
  144. $this->loadConfig($configPath);
  145. }
  146. /**
  147. * Loads config file content into protected properties
  148. *
  149. * @param string $configPath Path to config file
  150. * @return void
  151. */
  152. protected function loadConfig($configPath) {
  153. if (is_readable($configPath)) {
  154. $configTmp = file_get_contents($configPath);
  155. if (!empty($configTmp)) {
  156. $this->currentConfig = explodeRows($configTmp);
  157. $this->parsedConfig = rcms_parse_ini_file($configPath, false);
  158. $this->extractComments();
  159. }
  160. }
  161. }
  162. /**
  163. * Extracts comments from config lines
  164. *
  165. * @return void
  166. */
  167. protected function extractComments() {
  168. $this->lineComments = array();
  169. foreach ($this->currentConfig as $lineNum => $line) {
  170. $line = trim($line);
  171. if (empty($line)) {
  172. continue;
  173. }
  174. // Check for standalone comments
  175. if (substr($line, 0, 1) === ';') {
  176. $this->lineComments[$lineNum] = $line;
  177. continue;
  178. }
  179. // Check for inline comments
  180. if (strpos($line, ';') !== false) {
  181. $parts = explode(';', $line, 2);
  182. $this->lineComments[$lineNum] = trim($parts[1]);
  183. }
  184. }
  185. }
  186. /**
  187. * Returns the current config as text for debugging
  188. *
  189. * @return string
  190. */
  191. protected function getConfigAsText() {
  192. $configContent = '';
  193. $processedOptions = array();
  194. // First, process all lines in their original order
  195. foreach ($this->currentConfig as $lineNum => $line) {
  196. $line = trim($line);
  197. if (empty($line)) {
  198. $configContent .= PHP_EOL;
  199. continue;
  200. }
  201. // Handle standalone comments
  202. if (substr($line, 0, 1) === ';') {
  203. $configContent .= $line . PHP_EOL;
  204. continue;
  205. }
  206. // Handle key-value pairs
  207. if (strpos($line, '=') !== false) {
  208. list($key, $value) = explode('=', $line, 2);
  209. $key = trim($key);
  210. $value = trim($value);
  211. // If this option exists in our parsed config, use the updated value
  212. if (isset($this->parsedConfig[$key])) {
  213. $newValue = $this->parsedConfig[$key];
  214. // Escape all non-numeric values (except 0 and 1) with quotes
  215. if (!is_numeric($newValue) and $newValue !== '0' and $newValue !== '1' and !empty($newValue)) {
  216. $newValue = '"' . $newValue . '"';
  217. }
  218. $line = $key . '=' . $newValue;
  219. }
  220. // Add inline comment if exists
  221. if (isset($this->lineComments[$lineNum])) {
  222. $line .= ' ;' . $this->lineComments[$lineNum];
  223. }
  224. $configContent .= $line . PHP_EOL;
  225. $processedOptions[] = $key;
  226. }
  227. }
  228. // Add any new options from spec file that weren't in the original config
  229. if (is_readable($this->specPath)) {
  230. $specData = rcms_parse_ini_file($this->specPath, true);
  231. foreach ($specData as $section => $props) {
  232. if (isset($props['OPTION']) and !in_array($props['OPTION'], $processedOptions)) {
  233. $option = $props['OPTION'];
  234. $value = '';
  235. // First try to get value from POST if available
  236. if (ubRouting::checkPost(array(self::FORM_SUBMIT_KEY))) {
  237. $submitId = ubRouting::post(self::FORM_SUBMIT_KEY);
  238. if ($submitId === $this->instanceId) {
  239. $postData = ubRouting::rawPost();
  240. $uniqueInputName = $option . '_' . $this->instanceId;
  241. if (isset($postData[$uniqueInputName])) {
  242. $value = $postData[$uniqueInputName];
  243. }
  244. }
  245. }
  246. // For non-TEXT and non-PASSWORD types, use default if value is empty
  247. if (empty($value) and isset($props['TYPE']) and !in_array($props['TYPE'], array('TEXT', 'PASSWORD')) and isset($props['DEFAULT'])) {
  248. $value = $props['DEFAULT'];
  249. }
  250. // Handle checkbox and trigger values
  251. if (isset($props['TYPE']) and ($props['TYPE'] === 'CHECKBOX' or $props['TYPE'] === 'TRIGGER')) {
  252. $values = !empty($props['VALUES']) ? explode(',', $props['VALUES']) : array('1', '0');
  253. $value = $value ? $values[0] : $values[1];
  254. }
  255. // Optional pre-save filter
  256. if (isset($props['SAVEFILTER'])) {
  257. $value=ubRouting::filters($value,$props['SAVEFILTER']);
  258. }
  259. // Escape all non-numeric values (except 0 and 1) with quotes, but only if not empty
  260. if (!is_numeric($value) and $value !== '0' and $value !== '1' and !empty($value)) {
  261. $value = '"' . $value . '"';
  262. }
  263. $line = $option . '=' . $value;
  264. $configContent .= $line . PHP_EOL;
  265. }
  266. }
  267. }
  268. $configContent = rtrim($configContent);
  269. return ($configContent . PHP_EOL);
  270. }
  271. /**
  272. * Saves current config back to file preserving comments
  273. *
  274. * @return string Empty string on success, error message on failure
  275. */
  276. protected function saveConfig() {
  277. if (!is_writable($this->configPath)) {
  278. return (__('Failed to save config file') . ': ' . $this->configPath);
  279. }
  280. $configContent = $this->getConfigAsText();
  281. if (file_put_contents($this->configPath, $configContent) === false) {
  282. return (__('Failed to write config file') . ': ' . $this->configPath);
  283. }
  284. return ('');
  285. }
  286. /**
  287. * Gets config value by key
  288. *
  289. * @param string $key Config key name
  290. * @param string $default Default value to return if key doesn't exist
  291. * @return mixed
  292. */
  293. protected function getValue($key, $default = false) {
  294. $result = $default;
  295. if (isset($this->parsedConfig[$key])) {
  296. $result = $this->parsedConfig[$key];
  297. }
  298. return $result;
  299. }
  300. /**
  301. * Sets config value for key
  302. *
  303. * @param string $key Config key name
  304. * @param string $value New value to set
  305. * @return bool
  306. */
  307. protected function setValue($key, $value) {
  308. $this->parsedConfig[$key] = $value;
  309. return true;
  310. }
  311. /**
  312. * Returns instance identifier
  313. *
  314. * @return string
  315. */
  316. public function getInstanceId() {
  317. return $this->instanceId;
  318. }
  319. /**
  320. * Sets form CSS class
  321. *
  322. * @param string $class CSS class name
  323. * @return void
  324. */
  325. public function setFormClass($class) {
  326. $this->formClass = $class;
  327. }
  328. /**
  329. * Renders form editor for config file based on spec file
  330. *
  331. * @return string
  332. */
  333. public function renderEditor() {
  334. $result = '';
  335. $errors = array();
  336. // Check if spec file exists and is readable
  337. if (!file_exists($this->specPath)) {
  338. $errors[] = __('Spec file does not exist') . ': ' . $this->specPath;
  339. } elseif (!is_readable($this->specPath)) {
  340. $errors[] = __('Spec file is not readable') . ': ' . $this->specPath;
  341. }
  342. // If there are errors, display them and return
  343. if (!empty($errors)) {
  344. foreach ($errors as $error) {
  345. $result .= $error . wf_delimiter(0);
  346. }
  347. return ($result);
  348. }
  349. // If spec file is readable, proceed with rendering the form
  350. if (is_readable($this->specPath)) {
  351. $specData = rcms_parse_ini_file($this->specPath, true);
  352. if (!empty($specData)) {
  353. foreach ($specData as $section => $sectionData) {
  354. // Skip if section data is not an array
  355. if (!is_array($sectionData)) {
  356. continue;
  357. }
  358. // Get the option name and properties
  359. $option = isset($sectionData['OPTION']) ? $sectionData['OPTION'] : '';
  360. if (empty($option)) {
  361. continue;
  362. }
  363. // Create unique input name by appending instance ID
  364. $uniqueInputName = $option . '_' . $this->instanceId;
  365. // Get current value from config file
  366. $currentValue = $this->getValue($option, false);
  367. // If option doesn't exist in config, use DEFAULT from spec for input state
  368. if ($currentValue === false and isset($sectionData['DEFAULT'])) {
  369. $currentValue = $sectionData['DEFAULT'];
  370. }
  371. // Label - use LABEL from spec if available, otherwise use OPTION name
  372. $labelText = (!empty($sectionData['LABEL'])) ? __($sectionData['LABEL']) : $option;
  373. // Input field based on type
  374. $type = isset($sectionData['TYPE']) ? $sectionData['TYPE'] : 'TEXT';
  375. switch ($type) {
  376. case 'TRIGGER':
  377. $values = !empty($sectionData['VALUES']) ? explode(',', $sectionData['VALUES']) : array('1', '0');
  378. $result .= wf_Trigger($uniqueInputName, $labelText, $currentValue);
  379. $result .= wf_delimiter(0);
  380. break;
  381. case 'CHECKBOX':
  382. $values = !empty($sectionData['VALUES']) ? explode(',', $sectionData['VALUES']) : array('1', '0');
  383. $isChecked = ($currentValue == $values[0]);
  384. $result .= wf_CheckInput($uniqueInputName, $labelText, false, $isChecked);
  385. $result .= wf_delimiter(0);
  386. break;
  387. case 'RADIO':
  388. $values = !empty($sectionData['VALUES']) ? explode(',', $sectionData['VALUES']) : array();
  389. $result .= wf_tag('label') . $labelText . wf_tag('label', true) . wf_delimiter(0);
  390. foreach ($values as $value) {
  391. $isChecked = ($currentValue == $value);
  392. $result .= wf_RadioInput($uniqueInputName, $value, $value, false, $isChecked);
  393. $result .= wf_delimiter(0);
  394. }
  395. break;
  396. case 'SELECT':
  397. $values = !empty($sectionData['VALUES']) ? explode(',', $sectionData['VALUES']) : array();
  398. $params = array();
  399. foreach ($values as $value) {
  400. $params[$value] = $value;
  401. }
  402. $result .= wf_Selector($uniqueInputName, $params, $labelText, $currentValue);
  403. $result .= wf_delimiter(0);
  404. break;
  405. case 'PASSWORD':
  406. $result .= wf_PasswordInput($uniqueInputName, $labelText, $currentValue, false, '', false);
  407. $result .= wf_delimiter(0);
  408. break;
  409. case 'TEXT':
  410. $pattern = (!empty($sectionData['PATTERN'])) ? $sectionData['PATTERN'] : '';
  411. $result .= wf_TextInput($uniqueInputName, $labelText, $currentValue, false, '', $pattern);
  412. $result .= wf_delimiter(0);
  413. break;
  414. case 'SLIDER':
  415. // Parse range from VALUES or use default 0..100
  416. $range = array(0, 100); // default range
  417. if (!empty($sectionData['VALUES'])) {
  418. $rangeStr = trim($sectionData['VALUES']);
  419. if (preg_match('/^(\d+)\.\.(\d+)$/', $rangeStr, $matches)) {
  420. $range = array(intval($matches[1]), intval($matches[2]));
  421. }
  422. }
  423. // Ensure current value is within range
  424. $currentValue = intval($currentValue);
  425. if ($currentValue < $range[0]) {
  426. $currentValue = $range[0];
  427. } elseif ($currentValue > $range[1]) {
  428. $currentValue = $range[1];
  429. }
  430. $result .= wf_SliderInput($uniqueInputName, $labelText, $currentValue, $range[0], $range[1]);
  431. $result .= wf_delimiter(0);
  432. break;
  433. default:
  434. $result .= wf_TextInput($uniqueInputName, $labelText, $currentValue);
  435. $result .= wf_delimiter(0);
  436. }
  437. }
  438. // Add hidden input to identify ConfigForge form submission and instance
  439. $result .= wf_HiddenInput(self::FORM_SUBMIT_KEY, $this->instanceId);
  440. // Submit button
  441. $result .= wf_Submit('Save');
  442. // Wrap in form
  443. $result = wf_Form('', 'POST', $result, $this->formClass);
  444. } else {
  445. $result .= __('Spec file is empty or invalid');
  446. }
  447. }
  448. return ($result);
  449. }
  450. /**
  451. * Returns the text representation of the edited config
  452. *
  453. * @return string
  454. */
  455. protected function getConfigText() {
  456. $configContent = '';
  457. $processedOptions = array();
  458. // First, process all lines in their original order
  459. foreach ($this->currentConfig as $lineNum => $line) {
  460. $line = trim($line);
  461. if (empty($line)) {
  462. $configContent .= PHP_EOL;
  463. continue;
  464. }
  465. // Handle standalone comments
  466. if (substr($line, 0, 1) === ';') {
  467. $configContent .= $line . PHP_EOL;
  468. continue;
  469. }
  470. // Handle key-value pairs
  471. if (strpos($line, '=') !== false) {
  472. list($key, $value) = explode('=', $line, 2);
  473. $key = trim($key);
  474. $value = trim($value);
  475. // If this option exists in our parsed config, use the updated value
  476. if (isset($this->parsedConfig[$key])) {
  477. $newValue = $this->parsedConfig[$key];
  478. // Escape all non-numeric values (except 0 and 1) with quotes
  479. if (!is_numeric($newValue) and $newValue !== '0' and $newValue !== '1' and !empty($newValue)) {
  480. $newValue = '"' . $newValue . '"';
  481. }
  482. $line = $key . '=' . $newValue;
  483. }
  484. // Add inline comment if exists
  485. if (isset($this->lineComments[$lineNum])) {
  486. $line .= ' ;' . $this->lineComments[$lineNum];
  487. }
  488. $configContent .= $line . PHP_EOL;
  489. $processedOptions[] = $key;
  490. }
  491. }
  492. // Add any new options from spec file that weren't in the original config
  493. if (is_readable($this->specPath)) {
  494. $specData = rcms_parse_ini_file($this->specPath, true);
  495. foreach ($specData as $section => $props) {
  496. if (isset($props['OPTION']) and !in_array($props['OPTION'], $processedOptions)) {
  497. $option = $props['OPTION'];
  498. $value = '';
  499. // First try to get value from POST if available
  500. if (ubRouting::checkPost(array(self::FORM_SUBMIT_KEY))) {
  501. $submitId = ubRouting::post(self::FORM_SUBMIT_KEY);
  502. if ($submitId === $this->instanceId) {
  503. $postData = ubRouting::rawPost();
  504. $uniqueInputName = $option . '_' . $this->instanceId;
  505. if (isset($postData[$uniqueInputName])) {
  506. $value = $postData[$uniqueInputName];
  507. }
  508. }
  509. }
  510. // If no POST value, try to get default from spec
  511. if (empty($value) and isset($props['DEFAULT'])) {
  512. $value = $props['DEFAULT'];
  513. }
  514. // Handle checkbox and trigger values
  515. if (isset($props['TYPE']) and ($props['TYPE'] === 'CHECKBOX' or $props['TYPE'] === 'TRIGGER')) {
  516. $values = !empty($props['VALUES']) ? explode(',', $props['VALUES']) : array('1', '0');
  517. $value = $value ? $values[0] : $values[1];
  518. }
  519. // Escape all non-numeric values (except 0 and 1) with quotes, but only if not empty
  520. if (!is_numeric($value) and $value !== '0' and $value !== '1' and !empty($value)) {
  521. $value = '"' . $value . '"';
  522. }
  523. $line = $option . '=' . $value;
  524. $configContent .= $line . PHP_EOL;
  525. }
  526. }
  527. }
  528. return $configContent;
  529. }
  530. /**
  531. * Process config editing request
  532. * Handles form submission and config saving in one place
  533. *
  534. * @return string Empty string on success, error message on failure
  535. */
  536. public function process() {
  537. // Check if this is a ConfigForge form submission for this instance
  538. if (!ubRouting::checkPost(array(self::FORM_SUBMIT_KEY))) {
  539. return ('');
  540. }
  541. $submitId = ubRouting::post(self::FORM_SUBMIT_KEY);
  542. if ($submitId !== $this->instanceId) {
  543. return ('');
  544. }
  545. $postData = ubRouting::rawPost();
  546. if (empty($postData)) {
  547. return (__('No data received'));
  548. }
  549. if (!is_readable($this->specPath)) {
  550. return (__('Spec file is not readable') . ': ' . $this->specPath);
  551. }
  552. $specData = rcms_parse_ini_file($this->specPath, true);
  553. if (empty($specData)) {
  554. return (__('Spec file is empty or invalid') . ': ' . $this->specPath);
  555. }
  556. $updated = false;
  557. // Process each option from spec file
  558. foreach ($specData as $section => $props) {
  559. if (!isset($props['OPTION'])) {
  560. continue;
  561. }
  562. $option = $props['OPTION'];
  563. $uniqueInputName = $option . '_' . $this->instanceId;
  564. // For checkboxes, handle both present and not present in POST data
  565. if (isset($props['TYPE']) and $props['TYPE'] === 'CHECKBOX') {
  566. $values = !empty($props['VALUES']) ? explode(',', $props['VALUES']) : array('1', '0');
  567. $value = isset($postData[$uniqueInputName]) ? $values[0] : $values[1];
  568. // Applying optional save filter if exists
  569. if (!empty($props['SAVEFILTER'])) {
  570. $value = ubRouting::filters($value, $props['SAVEFILTER']);
  571. }
  572. $this->setValue($option, $value);
  573. $updated = true;
  574. continue;
  575. }
  576. // For other types, process only if present in POST
  577. if (isset($postData[$uniqueInputName])) {
  578. $value = $postData[$uniqueInputName];
  579. // Handle trigger values
  580. if (isset($props['TYPE']) and $props['TYPE'] === 'TRIGGER') {
  581. $values = !empty($props['VALUES']) ? explode(',', $props['VALUES']) : array('1', '0');
  582. $value = $value ? $values[0] : $values[1];
  583. }
  584. // Validate value if validator exists
  585. if (!empty($props['VALIDATOR'])) {
  586. $validator = $props['VALIDATOR'];
  587. $validatorPassed = false;
  588. // Default validation error notice
  589. $onInvalidMessage = __('Validation failed for') . ' ' . $option;
  590. // Or custom one
  591. if (!empty($props['ONINVALID'])) {
  592. $onInvalidMessage = __($props['ONINVALID']);
  593. }
  594. // Check if validator is a method in this class
  595. if (method_exists($this, $validator)) {
  596. if (!$this->$validator($value)) {
  597. return ($onInvalidMessage);
  598. } else {
  599. $validatorPassed = true;
  600. }
  601. }
  602. // Check if validator is a global function
  603. if (function_exists($validator)) {
  604. if (!$validator($value)) {
  605. return ($onInvalidMessage);
  606. } else {
  607. $validatorPassed = true;
  608. }
  609. }
  610. // If validator set but neither method nor function found
  611. if (!$validatorPassed) {
  612. return (__('Validator method not found') . ': ' . $validator . ' ' . __('for option') . ' ' . $option);
  613. }
  614. }
  615. // Applying optional save filter if exists
  616. if (!empty($props['SAVEFILTER'])) {
  617. $value = ubRouting::filters($value, $props['SAVEFILTER']);
  618. }
  619. // Set the value in our parsed config
  620. $this->setValue($option, $value);
  621. $updated = true;
  622. }
  623. }
  624. if ($updated) {
  625. // Try to save and check for errors
  626. $saveResult = $this->saveConfig();
  627. if (!empty($saveResult)) {
  628. return ($saveResult); // Return error message if save failed
  629. }
  630. }
  631. return ('');
  632. }
  633. }