Warning: file_get_contents(/home/www/vvveb/demo/public/themes/landing//home/www/vvveb/demo/plugins/seo/config/schemas/post/article.json): Failed to open stream: No such file or directory in /home/www/vvveb/demo/system/component/component.php on line 569
Exception:
Array
(
    [message] => Vvveb\prepareJson(): Argument #1 ($array) must be of type array, null given, called in /home/www/vvveb/demo/system/component/component.php on line 575
    [file] => /home/www/vvveb/demo/system/functions.php
    [line_no] => 2945
    [line] => function prepareJson(array $array): array {	 // <==

    [lines] => Array
        (
            [0] =>  * Encodes attribute markers (@, @@), preserves empty arrays/objects, booleans, integers, and nulls

            [1] =>  * using type suffix markers (--array, --boolean, --int, --null, --object).

            [2] =>  *

            [3] =>  * @param array $array The array to prepare

            [4] =>  *

            [5] =>  * @return array The prepared array with encoded keys

            [6] =>  */

            [7] => function prepareJson(array $array): array {	 // <==

            [8] => 	if (! is_array($array)) {

            [9] => 		//return;

            [10] => 	}

            [11] => 	$helper = [];

            [12] => 

            [13] => 	foreach ($array as $key => $value) {

        )

    [trace] => #0 /home/www/vvveb/demo/system/component/component.php(575): Vvveb\prepareJson()
#1 /home/www/vvveb/demo/system/component/component.php(709): Vvveb\System\Component\Component->loadTemplateDocument()
#2 /home/www/vvveb/demo/system/component/component.php(127): Vvveb\System\Component\Component->generateRequiredComponents()
#3 /home/www/vvveb/demo/system/core/view.php(521): Vvveb\System\Component\Component->__construct()
#4 /home/www/vvveb/demo/plugins/seo/app.php(81): Vvveb\System\Core\View->render()
#5 /home/www/vvveb/demo/plugins/seo/app.php(178): Vvveb\Plugins\Seo\App->setSchema()
#6 /home/www/vvveb/demo/plugins/seo/app.php(213): Vvveb\Plugins\Seo\App->pageType()
#7 [internal function]: Vvveb\Plugins\Seo\App->page()
#8 /home/www/vvveb/demo/system/event.php(105): call_user_func_array()
#9 /home/www/vvveb/demo/app/controller/content/post.php(182): Vvveb\System\Event::trigger()
#10 [internal function]: Vvveb\Controller\Content\Post->index()
#11 /home/www/vvveb/demo/system/core/frontcontroller.php(256): call_user_func()
#12 /home/www/vvveb/demo/system/core/frontcontroller.php(329): Vvveb\System\Core\FrontController::call()
#13 /home/www/vvveb/demo/system/core/frontcontroller.php(403): Vvveb\System\Core\FrontController::redirect()
#14 /home/www/vvveb/demo/system/core/startup.php(541): Vvveb\System\Core\FrontController::dispatch()
#15 /home/www/vvveb/demo/index.php(170): Vvveb\System\Core\start()
#16 /home/www/vvveb/demo/public/index.php(28): include('...')
#17 {main}
    [code] => Array
        (
            [0] =>  

            [2] => /**

            [3] =>  * Vvveb

            [4] =>  *

            [5] =>  * Copyright (C) 2022  Ziadin Givan

            [6] =>  *

            [7] =>  * This program is free software: you can redistribute it and/or modify

            [8] =>  * it under the terms of the GNU Affero General Public License as

            [9] =>  * published by the Free Software Foundation, either version 3 of the

            [10] =>  * License, or (at your option) any later version.

            [11] =>  *

            [12] =>  * This program is distributed in the hope that it will be useful,

            [13] =>  * but WITHOUT ANY WARRANTY; without even the implied warranty of

            [14] =>  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

            [15] =>  * GNU Affero General Public License for more details.

            [16] =>  *

            [17] =>  * You should have received a copy of the GNU Affero General Public License

            [18] =>  * along with this program.  If not, see .

            [19] =>  *

            [20] =>  */

            [21] => 

            [22] => namespace Vvveb;

            [23] => 

            [24] => /**

            [25] =>  * Generate a URL from a route name or array of parameters.

            [26] =>  *

            [27] =>  * @param string|array $parameters      Route name or URL parameters

            [28] =>  * @param array|bool   $mergeParameters Parameters to merge with existing URL parameters

            [29] =>  * @param bool                       $useCurrentUrl   Whether to use the current URL as base

            [30] =>  *

            [31] =>  * @return string The generated URL

            [32] =>  */

            [33] => function url(string|array $parameters, array|bool $mergeParameters = false, bool $useCurrentUrl = true): string {

            [34] => 	if (is_string($parameters) && $parameters) {

            [35] => 		$result = '';

            [36] => 

            [37] => 		if (isset($mergeParameters['scheme'])) {

            [38] => 			$result .= $mergeParameters['scheme'] . ':';

            [39] => 			unset($mergeParameters['scheme']);

            [40] => 		}

            [41] => 

            [42] => 		if (isset($mergeParameters['host'])) {

            [43] => 			$result .= '//' . \Vvveb\System\Sites::url($mergeParameters['host']);

            [44] => 			unset($mergeParameters['host']);

            [45] => 

            [46] => 			if (isset($mergeParameters['scheme'])) {

            [47] => 				$result = $mergeParameters['scheme'] . ":$result";

            [48] => 				unset($mergeParameters['scheme']);

            [49] => 			}

            [50] => 		}

            [51] => 

            [52] => 		$path = $mergeParameters['path'] ?? SITE_PATH;

            [53] => 

            [54] => 		$url = System\Routes::url($parameters, $mergeParameters);

            [55] => 		$result .= (V_SUBDIR_INSTALL ? V_SUBDIR_INSTALL : '') . $path . ($url ?? '');

            [56] => 	} else {

            [57] => 		static $url       = '';

            [58] => 		static $urlParams = [];

            [59] => 

            [60] => 		if ($useCurrentUrl && ! $url) {

            [61] => 			$url = parse_url($_SERVER['REQUEST_URI'] ?? '');

            [62] => 

            [63] => 			if (isset($url['query'])) {

            [64] => 				parse_str($url['query'], $urlParams);

            [65] => 			}

            [66] => 		}

            [67] => 

            [68] => 		if (! is_array($parameters)) {

            [69] => 			$parameters = [];

            [70] => 		}

            [71] => 

            [72] => 		if ($mergeParameters) {

            [73] => 			if (is_array($mergeParameters)) {

            [74] => 				$mergeParameters = array_merge($urlParams, $mergeParameters);

            [75] => 			} else {

            [76] => 				$mergeParameters = $urlParams;

            [77] => 			}

            [78] => 

            [79] => 			if (is_array($mergeParameters)) {

            [80] => 				$parameters = array_merge($mergeParameters, $parameters);

            [81] => 			}

            [82] => 		}

            [83] => 

            [84] => 		$result = '';

            [85] => 		$path = $parameters['path'] ?? SITE_PATH;

            [86] => 

            [87] => 		if (isset($parameters['host'])) {

            [88] => 			$result .= '//' . \Vvveb\System\Sites::url($parameters['host']);

            [89] => 			unset($parameters['host']);

            [90] => 

            [91] => 			if (isset($parameters['scheme'])) {

            [92] => 				$result = $parameters['scheme'] . ":$result";

            [93] => 				unset($parameters['scheme']);

            [94] => 			}

            [95] => 		} else {

            [96] => 			if (! $useCurrentUrl) {

            [97] => 				$result .= (V_SUBDIR_INSTALL ? V_SUBDIR_INSTALL : '') . $path;

            [98] => 			}

            [99] => 		}

            [100] => 

            [101] => 		$result .= ($useCurrentUrl ? $url['path'] ?? '' : '') . ($parameters ? '?' . urldecode(http_build_query($parameters)) : '');

            [102] => 	}

            [103] => 

            [104] => 	return $result;

            [105] => }

            [106] => 

            [107] => /**

            [108] =>  * Get a configuration value or the Config instance.

            [109] =>  *

            [110] =>  * @param string|null $key     Config key to retrieve, or null for the Config instance

            [111] =>  * @param mixed       $default Default value if key is not found

            [112] =>  *

            [113] =>  * @return mixed Configuration value or Config instance

            [114] =>  */

            [115] => function config(?string $key = null, mixed $default = null): mixed {

            [116] => 	if (is_null($key)) {

            [117] => 		return System\Config::getInstance();

            [118] => 	}

            [119] => 

            [120] => 	return System\Config::getInstance()->get($key, $default);

            [121] => }

            [122] => 

            [123] => /**

            [124] =>  * Get a configuration value.

            [125] =>  *

            [126] =>  * @param string|null $key     Config key to retrieve, or null for all config

            [127] =>  * @param mixed       $default Default value if key is not found

            [128] =>  *

            [129] =>  * @return mixed Configuration value

            [130] =>  */

            [131] => function getConfig(?string $key = null, mixed $default = null): mixed {

            [132] => 	return System\Config::getInstance()->get($key, $default);

            [133] => }

            [134] => 

            [135] => /**

            [136] =>  * Set a configuration value.

            [137] =>  *

            [138] =>  * @param string $key   Config key to set

            [139] =>  * @param mixed  $value Value to set

            [140] =>  *

            [141] =>  * @return bool True on success

            [142] =>  */

            [143] => function setConfig(string $key, mixed $value = null): bool {

            [144] => 	return System\Config::getInstance()->set($key, $value);

            [145] => }

            [146] => 

            [147] => /**

            [148] =>  * Remove a configuration value.

            [149] =>  *

            [150] =>  * @param string $key Config key to remove

            [151] =>  *

            [152] =>  * @return mixed Result of the unset operation

            [153] =>  */

            [154] => function unsetConfig(string $key): mixed {

            [155] => 	return System\Config::getInstance()->unset($key);

            [156] => }

            [157] => 

            [158] => /**

            [159] =>  * Get a setting value by namespace and key.

            [160] =>  *

            [161] =>  * @param string            $namespace Setting namespace

            [162] =>  * @param string|array|null $key       Setting key or array of keys

            [163] =>  * @param mixed             $default   Default value if key is not found

            [164] =>  * @param int               $site_id   Site ID

            [165] =>  *

            [166] =>  * @return mixed Setting value

            [167] =>  */

            [168] => function getSetting(string $namespace, string|array|null $key = null, mixed $default = null, int $site_id = SITE_ID): mixed {

            [169] => 	return System\Setting::getInstance()->get($namespace, $key, $default, $site_id);

            [170] => }

            [171] => 

            [172] => /**

            [173] =>  * Set a setting value.

            [174] =>  *

            [175] =>  * @param string      $namespace Setting namespace

            [176] =>  * @param string|null $key       Setting key

            [177] =>  * @param mixed       $value     Value to set

            [178] =>  * @param int         $site_id   Site ID

            [179] =>  *

            [180] =>  * @return mixed Result of the set operation

            [181] =>  */

            [182] => function setSetting(string $namespace, ?string $key = null, mixed $value = null, int $site_id = SITE_ID): mixed {

            [183] => 	return System\Setting::getInstance()->set($namespace, $key, $value, $site_id);

            [184] => }

            [185] => 

            [186] => /**

            [187] =>  * Delete a setting.

            [188] =>  *

            [189] =>  * @param string      $namespace Setting namespace

            [190] =>  * @param string|null $key       Setting key

            [191] =>  * @param int         $site_id   Site ID

            [192] =>  *

            [193] =>  * @return mixed Result of the delete operation

            [194] =>  */

            [195] => function deleteSetting(string $namespace, ?string $key = null, int $site_id = SITE_ID): mixed {

            [196] => 	return System\Setting::getInstance()->delete($namespace, $key, $site_id);

            [197] => }

            [198] => 

            [199] => /**

            [200] =>  * Set multiple settings at once.

            [201] =>  *

            [202] =>  * @param string $namespace Setting namespace

            [203] =>  * @param array  $settings  Array of key-value pairs to set

            [204] =>  * @param int    $site_id   Site ID

            [205] =>  *

            [206] =>  * @return mixed Result of the set operation

            [207] =>  */

            [208] => function setMultiSetting(string $namespace, array $settings, int $site_id = SITE_ID): mixed {

            [209] => 	return System\Setting::getInstance()->setMulti($namespace, $settings, $site_id);

            [210] => }

            [211] => 

            [212] => /**

            [213] =>  * Get multiple setting content values.

            [214] =>  *

            [215] =>  * @param int               $site_id     Site ID

            [216] =>  * @param string            $namespace   Setting namespace

            [217] =>  * @param array|string|null $key         Setting key or array of keys

            [218] =>  * @param array             $default     Default value if keys are not found

            [219] =>  * @param int|false         $language_id Language ID or false for default

            [220] =>  *

            [221] =>  * @return mixed Setting content values

            [222] =>  */

            [223] => function getMultiSettingContent(int $site_id, string $namespace, array|string|null $key = null, array $default = [], int|false $language_id = false): mixed {

            [224] => 	return System\Meta\SettingContent::getInstance()->getMulti($site_id, $namespace, $key, $default, $language_id);

            [225] => }

            [226] => 

            [227] => /**

            [228] =>  * Set multiple setting content values.

            [229] =>  *

            [230] =>  * @param int   $site_id Site ID

            [231] =>  * @param mixed $meta    Setting content data to set

            [232] =>  *

            [233] =>  * @return mixed Result of the set operation

            [234] =>  */

            [235] => function setMultiSettingContent(int $site_id, mixed $meta): mixed {

            [236] => 	return System\Meta\SettingContent::getInstance()->setMulti($site_id, $meta);

            [237] => }

            [238] => 

            [239] => /**

            [240] =>  * Get post metadata.

            [241] =>  *

            [242] =>  * @param int         $post_id   Post ID

            [243] =>  * @param string      $namespace Metadata namespace

            [244] =>  * @param string|null $key       Metadata key

            [245] =>  * @param mixed       $default   Default value if key is not found

            [246] =>  *

            [247] =>  * @return mixed Metadata value

            [248] =>  */

            [249] => function getPostMeta(int $post_id, string $namespace, string|null $key = null, mixed $default = null): mixed {

            [250] => 	return System\Meta\PostMeta::getInstance()->get($post_id, $namespace, $key, $default);

            [251] => }

            [252] => 

            [253] => /**

            [254] =>  * Set post metadata.

            [255] =>  *

            [256] =>  * @param int         $post_id   Post ID

            [257] =>  * @param string      $namespace Metadata namespace

            [258] =>  * @param string|null $key       Metadata key

            [259] =>  * @param mixed       $value     Value to set

            [260] =>  *

            [261] =>  * @return mixed Result of the set operation

            [262] =>  */

            [263] => function setPostMeta(int $post_id, string $namespace, string|null $key = null, mixed $value = null): mixed {

            [264] => 	return System\Meta\PostMeta::getInstance()->set($post_id, $namespace, $key, $value);

            [265] => }

            [266] => 

            [267] => /**

            [268] =>  * Delete post metadata.

            [269] =>  *

            [270] =>  * @param int         $post_id   Post ID

            [271] =>  * @param string      $namespace Metadata namespace

            [272] =>  * @param string|null $key       Metadata key

            [273] =>  *

            [274] =>  * @return mixed Result of the delete operation

            [275] =>  */

            [276] => function deletePostMeta(int $post_id, string $namespace, string|null $key = null): mixed {

            [277] => 	return System\Meta\PostMeta::getInstance()->delete($post_id, $namespace, $key);

            [278] => }

            [279] => 

            [280] => /**

            [281] =>  * Set multiple post metadata values at once.

            [282] =>  *

            [283] =>  * @param int    $post_id   Post ID

            [284] =>  * @param string $namespace Metadata namespace

            [285] =>  * @param mixed  $meta      Metadata data to set

            [286] =>  *

            [287] =>  * @return mixed Result of the set operation

            [288] =>  */

            [289] => function setMultiPostMeta(int $post_id, string $namespace, mixed $meta): mixed {

            [290] => 	return System\Meta\PostMeta::getInstance()->setMulti($post_id, $namespace, $meta);

            [291] => }

            [292] => 

            [293] => /**

            [294] =>  * Get post content metadata.

            [295] =>  *

            [296] =>  * @param int         $post_id     Post ID

            [297] =>  * @param string      $namespace   Metadata namespace

            [298] =>  * @param string|null $key         Metadata key

            [299] =>  * @param mixed       $default     Default value if key is not found

            [300] =>  * @param int|false   $language_id Language ID or false for default

            [301] =>  *

            [302] =>  * @return mixed Metadata value

            [303] =>  */

            [304] => function getPostContentMeta(int $post_id, string $namespace, string|null $key = null, mixed $default = null, int|false $language_id = false): mixed {

            [305] => 	return System\Meta\PostContentMeta::getInstance()->get($post_id, $namespace, $key, $default, $language_id);

            [306] => }

            [307] => 

            [308] => /**

            [309] =>  * Get multiple post content metadata values.

            [310] =>  *

            [311] =>  * @param int               $post_id     Post ID

            [312] =>  * @param string            $namespace   Metadata namespace

            [313] =>  * @param array|string|null $key         Metadata key or array of keys

            [314] =>  * @param array             $default     Default value if keys are not found

            [315] =>  * @param int|false         $language_id Language ID or false for default

            [316] =>  *

            [317] =>  * @return mixed Metadata values

            [318] =>  */

            [319] => function getMultiPostContentMeta(int $post_id, string $namespace, array|string|null $key = null, array $default = [], int|false $language_id = false): mixed {

            [320] => 	return System\Meta\PostContentMeta::getInstance()->getMulti($post_id, $namespace, $key, $default, $language_id);

            [321] => }

            [322] => 

            [323] => /**

            [324] =>  * Get multiple product content metadata values.

            [325] =>  *

            [326] =>  * @param int               $product_id  Product ID

            [327] =>  * @param string            $namespace   Metadata namespace

            [328] =>  * @param array|string|null $key         Metadata key or array of keys

            [329] =>  * @param array             $default     Default value if keys are not found

            [330] =>  * @param int|false         $language_id Language ID or false for default

            [331] =>  *

            [332] =>  * @return mixed Metadata values

            [333] =>  */

            [334] => function getMultiProductContentMeta(int $product_id, string $namespace, array|string|null $key = null, array $default = [], int|false $language_id = false): mixed {

            [335] => 	return System\Meta\ProductContentMeta::getInstance()->getMulti($product_id, $namespace, $key, $default, $language_id);

            [336] => }

            [337] => 

            [338] => /**

            [339] =>  * Set post content metadata.

            [340] =>  *

            [341] =>  * @param int         $post_id     Post ID

            [342] =>  * @param string      $namespace   Metadata namespace

            [343] =>  * @param string|null $key         Metadata key

            [344] =>  * @param mixed       $value       Value to set

            [345] =>  * @param int|false   $language_id Language ID or false for default

            [346] =>  *

            [347] =>  * @return mixed Result of the set operation

            [348] =>  */

            [349] => function setPostContentMeta(int $post_id, string $namespace, string|null $key = null, mixed $value = null, int|false $language_id = false): mixed {

            [350] => 	return System\Meta\PostContentMeta::getInstance()->set($post_id, $namespace, $key, $value, $language_id);

            [351] => }

            [352] => /**

            [353] =>  * Set product content metadata.

            [354] =>  *

            [355] =>  * @param int         $product_id  Product ID

            [356] =>  * @param string      $namespace   Metadata namespace

            [357] =>  * @param string|null $key         Metadata key

            [358] =>  * @param mixed       $value       Value to set

            [359] =>  * @param int|false   $language_id Language ID or false for default

            [360] =>  *

            [361] =>  * @return mixed Result of the set operation

            [362] =>  */

            [363] => function setProductContentMeta(int $product_id, string $namespace, string|null $key = null, mixed $value = null, int|false $language_id = false): mixed {

            [364] => 	return System\Meta\ProductContentMeta::getInstance()->set($product_id, $namespace, $key, $value, $language_id);

            [365] => }

            [366] => 

            [367] => /**

            [368] =>  * Delete post content metadata.

            [369] =>  *

            [370] =>  * @param int         $post_id     Post ID

            [371] =>  * @param string      $namespace   Metadata namespace

            [372] =>  * @param string|null $key         Metadata key

            [373] =>  * @param int|false   $language_id Language ID or false for default

            [374] =>  *

            [375] =>  * @return mixed Result of the delete operation

            [376] =>  */

            [377] => function deletePostContentMeta(int $post_id, string $namespace, string|null $key = null, int|false $language_id = false): mixed {

            [378] => 	return System\Meta\PostContentMeta::getInstance()->delete($post_id, $namespace, $key, $language_id);

            [379] => }

            [380] => 

            [381] => /**

            [382] =>  * Delete product content metadata.

            [383] =>  *

            [384] =>  * @param int         $product_id  Product ID

            [385] =>  * @param string      $namespace   Metadata namespace

            [386] =>  * @param string|null $key         Metadata key

            [387] =>  * @param int|false   $language_id Language ID or false for default

            [388] =>  *

            [389] =>  * @return mixed Result of the delete operation

            [390] =>  */

            [391] => function deleteProductContentMeta(int $product_id, string $namespace, string|null $key = null, int|false $language_id = false): mixed {

            [392] => 	return System\Meta\ProductContentMeta::getInstance()->delete($product_id, $namespace, $key, $language_id);

            [393] => }

            [394] => 

            [395] => /**

            [396] =>  * Set multiple post content metadata values at once.

            [397] =>  *

            [398] =>  * @param int   $post_id Post ID

            [399] =>  * @param mixed $meta    Metadata data to set

            [400] =>  *

            [401] =>  * @return mixed Result of the set operation

            [402] =>  */

            [403] => function setMultiPostContentMeta(int $post_id, mixed $meta): mixed {

            [404] => 	return System\Meta\PostContentMeta::getInstance()->setMulti($post_id, $meta);

            [405] => }

            [406] => 

            [407] => /**

            [408] =>  * Set multiple product content metadata values at once.

            [409] =>  *

            [410] =>  * @param int   $product_id Product ID

            [411] =>  * @param mixed $meta       Metadata data to set

            [412] =>  *

            [413] =>  * @return mixed Result of the set operation

            [414] =>  */

            [415] => function setMultiProductContentMeta(int $product_id, mixed $meta): mixed {

            [416] => 	return System\Meta\ProductContentMeta::getInstance()->setMulti($product_id, $meta);

            [417] => }

            [418] => 

            [419] => /**

            [420] =>  * Get the current template name.

            [421] =>  *

            [422] =>  * @return string|false|null Template name, or false/null if not set

            [423] =>  */

            [424] => function getCurrentTemplate(): string|false|null {

            [425] => 	return System\Core\View :: getInstance()->template();

            [426] => }

            [427] => 

            [428] => /**

            [429] =>  * Get route data for a given URL.

            [430] =>  *

            [431] =>  * @param string $url The URL to look up

            [432] =>  *

            [433] =>  * @return array|false Route data array, or false if not found

            [434] =>  */

            [435] => function getUrlRoute(string $url): array|false {

            [436] => 	$urlData = \Vvveb\System\Routes::getUrlData($url);

            [437] => 

            [438] => 	return $urlData;

            [439] => }

            [440] => 

            [441] => /**

            [442] =>  * Get the current request URI.

            [443] =>  *

            [444] =>  * @return string Current request URI

            [445] =>  */

            [446] => function getCurrentUrl(): string {

            [447] => 	return $_SERVER['REQUEST_URI'] ?? '';

            [448] => }

            [449] => 

            [450] => /**

            [451] =>  * Get the admin path URL.

            [452] =>  *

            [453] =>  * @return string Admin path with leading and trailing slashes

            [454] =>  */

            [455] => function adminPath(): string {

            [456] => 	return (V_SUBDIR_INSTALL ? V_SUBDIR_INSTALL : '') . '/' . \Vvveb\config('admin.path', 'admin') . '/';

            [457] => }

            [458] => 

            [459] => /**

            [460] =>  * Get the public URL path.

            [461] =>  *

            [462] =>  * @return string Public URL path

            [463] =>  */

            [464] => function publicUrlPath(): string {

            [465] => 	$public_path = PUBLIC_PATH;

            [466] => 

            [467] => 	if (V_SUBDIR_INSTALL) {

            [468] => 		$public_path = str_replace(V_SUBDIR_INSTALL, '', $public_path);

            [469] => 	}

            [470] => 

            [471] => 	if ($public_path == '/public/' || $public_path == '/public/admin/') {

            [472] => 		return PUBLIC_PATH;

            [473] => 	} else {

            [474] => 		return '/';

            [475] => 	}

            [476] => }

            [477] => 

            [478] => /**

            [479] =>  * Get the current theme URL path.

            [480] =>  *

            [481] =>  * @return string Theme URL path

            [482] =>  */

            [483] => function themeUrlPath(): string {

            [484] => 	$theme = \Vvveb\System\Core\View::getInstance()->getTheme();

            [485] => 	//$theme = 'landing';

            [486] => 	if (APP == 'app') {

            [487] => 		$path = PUBLIC_THEME_PATH . 'themes/' . $theme . '/';

            [488] => 	} else {

            [489] => 		$path = PUBLIC_THEME_PATH . $theme . '/';

            [490] => 	}

            [491] => 

            [492] => 	return $path;

            [493] => }

            [494] => 

            [495] => /**

            [496] =>  * Escape a URL for HTML output.

            [497] =>  *

            [498] =>  * @param string $url URL to escape

            [499] =>  *

            [500] =>  * @return string Escaped URL

            [501] =>  */

            [502] => function escUrl(string $url): string {

            [503] => 	return htmlspecialchars($url, ENT_QUOTES, 'UTF-8');

            [504] => }

            [505] => 

            [506] => /**

            [507] =>  * Escape an attribute value for HTML output.

            [508] =>  *

            [509] =>  * @param string $attr Attribute value to escape

            [510] =>  *

            [511] =>  * @return string Escaped attribute value

            [512] =>  */

            [513] => function escAttr(string $attr): string {

            [514] => 	return htmlspecialchars($attr);

            [515] => }

            [516] => 

            [517] => /**

            [518] =>  * Escape a string for HTML output.

            [519] =>  *

            [520] =>  * @param string $url String to escape

            [521] =>  *

            [522] =>  * @return string Escaped string

            [523] =>  */

            [524] => function escHtml(string $url): string {

            [525] => 	return htmlspecialchars($url);

            [526] => }

            [527] => 

            [528] => /**

            [529] =>  * Get an environment variable value.

            [530] =>  *

            [531] =>  * @param string $key     Environment variable name

            [532] =>  * @param mixed  $default Default value if variable is not set

            [533] =>  *

            [534] =>  * @return mixed Environment variable value or default

            [535] =>  */

            [536] => function env(string $key, mixed $default = null): mixed {

            [537] => 	if ($env = getenv($key)) {

            [538] => 		return $env;

            [539] => 	} else {

            [540] => 		return $default;

            [541] => 	}

            [542] => }

            [543] => 

            [544] => /**

            [545] =>  * Convert a date to a human-friendly relative format.

            [546] =>  *

            [547] =>  * @param string|int $date Date string or Unix timestamp

            [548] =>  *

            [549] =>  * @return string|int Human-friendly date string, or original timestamp if too far in the future

            [550] =>  */

            [551] => function friendlyDate(string|int $date): string|int {

            [552] => 	$fileformats = [

            [553] => 		1          => static fn ($i) => __('%d second', '%d seconds', $i),

            [554] => 		60         => static fn ($i) => __('%d minute', '%d minutes', $i),

            [555] => 		3600       => static fn ($i) => __('%d hour', '%d hours', $i),

            [556] => 		86400      => static fn ($i) => __('%d day', '%d days', $i),

            [557] => 		604800     => static fn ($i) => __('%d week', '%d weeks', $i),

            [558] => 		2592000    => static fn ($i) => __('%d month', '%d months', $i),

            [559] => 		31536000   => static fn ($i) => __('%d year', '%d years', $i),

            [560] => 		315360000  => static fn ($i) => __('%d decade', '%d decades', $i),

            [561] => 		3153600000 => static fn ($i) => __('%d century', '%d centuries', $i),

            [562] => 	];

            [563] => 

            [564] => 	$time_direction = __(' ago');

            [565] => 

            [566] => 	if (is_string($date)) {

            [567] => 		$date = strtotime($date);

            [568] => 	}

            [569] => 

            [570] => 	$diff           = time() - $date + 10;

            [571] => 

            [572] => 	if ($diff < 0) {

            [573] => 		$time_direction = __(' from now');

            [574] => 		$diff           = abs($diff);

            [575] => 	}

            [576] => 

            [577] => 	$lastTime = 1;

            [578] => 	$lastText = $fileformats[1]($diff);

            [579] => 

            [580] => 	foreach ($fileformats as $time => $text) {

            [581] => 		if ($diff < $time) {

            [582] => 			$units = floor($diff / $lastTime);

            [583] => 

            [584] => 			return sprintf($lastText($units), $units) . $time_direction;

            [585] => 		}

            [586] => 		$lastText = $text;

            [587] => 		$lastTime = $time;

            [588] => 	}

            [589] => 

            [590] => 	return $date;

            [591] => }

            [592] => 

            [593] => /**

            [594] =>  * Convert dot notation to array key notation.

            [595] =>  *

            [596] =>  * Converts var.key1.key2 to var['key1']['key2'].

            [597] =>  *

            [598] =>  * @param string $key Dot-notation key string

            [599] =>  *

            [600] =>  * @return string Array key notation string

            [601] =>  */

            [602] => function dotToArrayKey(string $key): string {

            [603] => 	//var.key1.key2 > var['key1']['key2']

            [604] => 	//var.key1 > var['key1']

            [605] => 

            [606] => 	return preg_replace_callback('/\.([-_\w]+)/', function ($matches) {

            [607] => 		return "['" . str_replace("'", "\'", $matches[1]) . "']";

            [608] => 	}, $key);

            [609] => }

            [610] => 

            [611] => /**

            [612] =>  * Convert form name bracket notation to array key notation.

            [613] =>  *

            [614] =>  * Converts name[key] to ['name']['key'].

            [615] =>  *

            [616] =>  * @param string $key Form name bracket notation string

            [617] =>  *

            [618] =>  * @return string Array key notation string

            [619] =>  */

            [620] => function postNameToArrayKey(string $key): string {

            [621] => 	$key = str_replace(['][', '[', ']'] , ['.', '.', ''], $key);

            [622] => 

            [623] => 	return '[\'' . str_replace('.', '\'][\'', $key) . '\']';

            [624] => }

            [625] => 

            [626] => /**

            [627] =>  * Prefix all array keys with a given prefix.

            [628] =>  *

            [629] =>  * @param string $prefix Prefix to add to each key

            [630] =>  * @param array  &$array Array to modify by reference

            [631] =>  *

            [632] =>  * @return array Modified array with prefixed keys

            [633] =>  */

            [634] => function prefixArrayKeys(string $prefix, array &$array): array {

            [635] => 	if (is_array($array)) {

            [636] => 		foreach ($array as $k => $v) {

            [637] => 			$array["$prefix$k"] = $v;

            [638] => 			unset($array[$k]);

            [639] => 		}

            [640] => 	}

            [641] => 

            [642] => 	return $array;

            [643] => }

            [644] => 

            [645] => /**

            [646] =>  * Sanitize text by encoding special characters.

            [647] =>  *

            [648] =>  * @param string $data Text to sanitize

            [649] =>  *

            [650] =>  * @return string Sanitized text

            [651] =>  */

            [652] => function filterText(string $data): string {

            [653] => 	return urldecode(filter_var($data, FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_ENCODE_HIGH | FILTER_FLAG_ENCODE_LOW));

            [654] => }

            [655] => 

            [656] => /**

            [657] =>  * Get or set session data.

            [658] =>  *

            [659] =>  * When a string is passed, retrieves the session value for that key. When an array is passed, sets all key-value pairs.

            [660] =>  *

            [661] =>  * @param string|array $data    Session key to get, or array of key-value pairs to set

            [662] =>  * @param mixed        $default Default value to return if key is not found

            [663] =>  *

            [664] =>  * @return mixed Session value when getting, or default value

            [665] =>  */

            [666] => function session(string|array $data, mixed $default = null): mixed {

            [667] => 	$session = System\Session::getInstance();

            [668] => 

            [669] => 	if (! $session) {

            [670] => 		return $default;

            [671] => 	}

            [672] => 

            [673] => 	if (is_array($data)) {

            [674] => 		foreach ($data as $key => $value) {

            [675] => 			$session->set($key, $value);

            [676] => 		}

            [677] => 	} else {

            [678] => 		$value = $session->get($data);

            [679] => 

            [680] => 		if ($value === null) {

            [681] => 			return $default;

            [682] => 		}

            [683] => 

            [684] => 		return $value;

            [685] => 	}

            [686] => 

            [687] => 	return $default;

            [688] => }

            [689] => 

            [690] => /**

            [691] =>  * Regex filter to allow only matched content.

            [692] =>  *

            [693] =>  * @param string $regex        Regular expression pattern

            [694] =>  * @param string $input        Input string to filter

            [695] =>  * @param int    $maxInputSize Maximum input size to test

            [696] =>  *

            [697] =>  * @return string|false Matched content, or false if no match

            [698] =>  */

            [699] => function filter(string $regex, string $input, int $maxInputSize = 100): string|false {

            [700] => 	$matches = [];

            [701] => 

            [702] => 	if (preg_match($regex, substr($input, 0, $maxInputSize), $matches)) {

            [703] => 		return $matches[0];

            [704] => 	} else {

            [705] => 		return false;

            [706] => 	}

            [707] => }

            [708] => 

            [709] => /**

            [710] =>  * Shortcut for preg_match to return the matched parameter directly.

            [711] =>  * 

            [712] =>  * @param string  $regex

            [713] =>  * @param string  $input

            [714] =>  * @param integer $level [false] 

            [715] =>  *

            [716] =>  * @return mixed

            [717] =>  */

            [718] => function pregMatch(string $regex, string $input, int|false $level = false): mixed {

            [719] => 	$matches = [];

            [720] => 

            [721] => 	//if (preg_match($regex, $input, $matches, PREG_UNMATCHED_AS_NULL | PREG_PATTERN_ORDER)) {

            [722] => 	if (preg_match($regex, $input, $matches, PREG_UNMATCHED_AS_NULL)) {

            [723] => 		if ($level !== false) {

            [724] => 			return $matches[$level];

            [725] => 		}

            [726] => 

            [727] => 		return $matches;

            [728] => 	} else {

            [729] => 		return false;

            [730] => 	}

            [731] => }

            [732] => 

            [733] => /**

            [734] =>  * Shortcut for preg_match_all to return the matched parameters directly.

            [735] =>  * 

            [736] =>  * @param string  $regex

            [737] =>  * @param string  $input

            [738] =>  * @param integer $level [false] 

            [739] =>  *

            [740] =>  * @return mixed

            [741] =>  */

            [742] => function pregMatchAll(string $regex, string $input, int|false $level = false): mixed {

            [743] => 	$matches = [];

            [744] => 

            [745] => 	if (preg_match_all($regex, $input, $matches, PREG_UNMATCHED_AS_NULL | PREG_PATTERN_ORDER)) {

            [746] => 		if ($level !== false) {

            [747] => 			return $matches[$level];

            [748] => 		}

            [749] => 

            [750] => 		return $matches;

            [751] => 	} else {

            [752] => 		return false;

            [753] => 	}

            [754] => }

            [755] => 

            [756] => /**

            [757] =>  * Validates that input is in the allowed values list.

            [758] =>  *

            [759] =>  * @param mixed $input         The input value to validate.

            [760] =>  * @param array $allowedValues The list of allowed values.

            [761] =>  *

            [762] =>  * @return mixed The input value if valid, null otherwise.

            [763] =>  */

            [764] => function arrayAllowValues(mixed $input, array $allowedValues): mixed {

            [765] => 	if (! in_array($input, $allowedValues)) {

            [766] => 		return null;

            [767] => 	} else {

            [768] => 		return $input;

            [769] => 	}

            [770] => }

            [771] => 

            [772] => /**

            [773] =>  * Get values from multidimensional arrays based on path.

            [774] =>  * For example for array ['item' => ['desc' => ['name' => 'test']]] the path "item.desc.name" will return "test".

            [775] =>  *

            [776] =>  * @param array  $a       The array to search.

            [777] =>  * @param string $path    The dot-separated path to the value.

            [778] =>  * @param mixed  $default The default value if path is not found.

            [779] =>  * @param string $token   The token used to separate path segments.

            [780] =>  *

            [781] =>  * @return mixed The value at the given path or the default value.

            [782] =>  */

            [783] => function arrayPath(array $a, string $path, mixed $default = null, string $token = '.'): mixed {

            [784] => 	$p = strtok($path, $token);

            [785] => 

            [786] => 	while ($p !== false) {

            [787] => 		if (! isset($a[$p])) {

            [788] => 			return $default;

            [789] => 		}

            [790] => 

            [791] => 		$a = $a[$p];

            [792] => 		$p = strtok($token);

            [793] => 	}

            [794] => 

            [795] => 	return $a;

            [796] => }

            [797] => 

            [798] => /**

            [799] =>  * Converts slug/text to human readable format.

            [800] =>  *

            [801] =>  * @param string $text The text to convert.

            [802] =>  *

            [803] =>  * @return string The human readable text.

            [804] =>  */

            [805] => function humanReadable(string $text): string {

            [806] => 	return ucfirst(str_replace(['_', '-', '/', '[', ']', '.'], [' ', ' ', ' - ', ' ', ' ', ' '], trim($text, ' /\-_')));

            [807] => }

            [808] => 

            [809] => /**

            [810] =>  * Cleans text for URL usage without transliteration.

            [811] =>  *

            [812] =>  * @param string $text    The text to clean.

            [813] =>  * @param string $divider The divider character to use.

            [814] =>  *

            [815] =>  * @return string The cleaned URL string.

            [816] =>  */

            [817] => function cleanUrl(string $text, string $divider = '-'): string {

            [818] => 	// replace non letter or digits by divider

            [819] => 	$text = preg_replace('/[^\pL\d]+/u', $divider, $text);

            [820] => 

            [821] => 	// remove unwanted characters

            [822] => 	$text = preg_replace('/[^\\-\w]+/', '', $text);

            [823] => 

            [824] => 	$text = trim($text, $divider);

            [825] => 

            [826] => 	return $text;

            [827] => }

            [828] => 

            [829] => /**

            [830] =>  * Creates URL-safe slug from text with transliteration.

            [831] =>  *

            [832] =>  * @param string $text    The text to slugify.

            [833] =>  * @param string $divider The divider character to use.

            [834] =>  *

            [835] =>  * @return string The URL-safe slug.

            [836] =>  */

            [837] => function slugify(string $text, string $divider = '-'): string {

            [838] => 	if (! $text) {

            [839] => 		return $text;

            [840] => 	}

            [841] => 	// replace non letter or digits by divider

            [842] => 	$text = preg_replace('/[^\pL\d]+/u', $divider, $text);

            [843] => 

            [844] => 	// transliterate

            [845] => 	if (function_exists('iconv')) {

            [846] => 		$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);

            [847] => 	}

            [848] => 

            [849] => 	// remove unwanted characters

            [850] => 	$text = preg_replace('/[^-\w]+/', '', $text);

            [851] => 

            [852] => 	// trim

            [853] => 	$text = trim($text, $divider);

            [854] => 

            [855] => 	// remove duplicate divider

            [856] => 	$text = preg_replace('/-+/', $divider, $text);

            [857] => 

            [858] => 	// lowercase

            [859] => 	$text = strtolower($text);

            [860] => 

            [861] => 	return $text;

            [862] => }

            [863] => 

            [864] => if ((! defined('GETTEXT') || GETTEXT == true) && function_exists('_')) {

            [865] => 	/**

            [866] => 	 * Translates text using gettext.

            [867] => 	 *

            [868] => 	 * @param string       $text    The text to translate.

            [869] => 	 * @param string|false $plural  The plural form of the text.

            [870] => 	 * @param int|false    $count   The count for plural translation.

            [871] => 	 * @param bool         $nocache Whether to bypass the translation cache.

            [872] => 	 *

            [873] => 	 * @return string The translated text.

            [874] => 	 */

            [875] => 	function __($text, string|false $plural = false, int|false $count = false, bool $nocache = false): string {

            [876] => 		if ($plural) {

            [877] => 			$translation = ngettext($text, $plural, $count);

            [878] => 		} else {

            [879] => 			$translation = gettext($text);

            [880] => 		}

            [881] => 

            [882] => 		//list($text, $plural, $count, $translation) = System\Event::trigger(__NAMESPACE__, '__', $text, $plural, $count, $translation);

            [883] => 

            [884] => 		return $translation;

            [885] => 	}

            [886] => } else {

            [887] => 	/**

            [888] => 	 * Translates text using custom PO file parser.

            [889] => 	 *

            [890] => 	 * @param string       $text    The text to translate.

            [891] => 	 * @param string|false $plural  The plural form of the text.

            [892] => 	 * @param int|false    $count   The count for plural translation.

            [893] => 	 * @param bool         $nocache Whether to bypass the translation cache.

            [894] => 	 *

            [895] => 	 * @return string The translated text.

            [896] => 	 */

            [897] => 	function __($text, string|false $plural = false, int|false $count = false, bool $nocache = false): string {

            [898] => 		static $translations;

            [899] => 

            [900] => 		if (($translations === null || $nocache) && ! isEditor()) {

            [901] => 			$translations = [];

            [902] => 			$lang = System\Locale::getLanguage();

            [903] => 

            [904] => 			$cacheFile = DIR_CACHE . $lang . '-translations.php';

            [905] => 

            [906] => 			if (file_exists($cacheFile)) {

            [907] => 				$translations = require $cacheFile;

            [908] => 			} else {

            [909] => 				$folder = DIR_ROOT . 'locale' . DS . $lang . DS . 'LC_MESSAGES' . DS;

            [910] => 				if (is_dir($folder)) {

            [911] => 					require_once DIR_SYSTEM . 'functions' . DS . 'php-mo.php';

            [912] => 

            [913] => 					foreach (['vvveb', 'user'] as $domain) {

            [914] => 						$poFile = $folder . $domain . '.po';

            [915] => 						if (file_exists($poFile) && ($t = phpmo_parse_po_file($poFile) ?: []) && is_array($t)) {

            [916] => 							$translations = array_merge($translations, $t);

            [917] => 						}

            [918] => 					}

            [919] => 

            [920] => 					foreach ($translations as $id => &$msg) {

            [921] => 						$msg = $msg['msgstr'];

            [922] => 					}

            [923] => 

            [924] => 					file_put_contents($cacheFile, ' 				}

            [926] => 			}

            [927] => 		}

            [928] => 

            [929] => 		if (isset($translations[$text])) {

            [930] => 			if ($count && $plural) {

            [931] => 				$translation = $translations[$text][1] ?? $translations[$text][0] ?? $text;

            [932] => 			} else {

            [933] => 				$translation = $translations[$text][0] ?? $text;

            [934] => 			}

            [935] => 		} else {

            [936] => 			$translation = $text;

            [937] => 		}

            [938] => 

            [939] => 		//list($text, $plural, $count, $translation) = System\Event::trigger(__NAMESPACE__, '__', $text, $plural, $count, $translation);

            [940] => 

            [941] => 		return $translation;

            [942] => 	}

            [943] => }

            [944] => 

            [945] => /**

            [946] =>  * Checks if the current user is an admin.

            [947] =>  *

            [948] =>  * @return bool True if the current user is an admin, false otherwise.

            [949] =>  */

            [950] => function isAdmin(): bool {

            [951] => 	return System\User\Admin::current() ? true : false;

            [952] => }

            [953] => 

            [954] => /**

            [955] =>  * Converts a CSS selector to an XPath query.

            [956] =>  *

            [957] =>  * @param string $selector The CSS selector to convert.

            [958] =>  *

            [959] =>  * @return string The resulting XPath query.

            [960] =>  */

            [961] => function cssToXpath(string $selector): string {

            [962] => 	//if already xpath don't transform

            [963] => 	//if (substr_compare($selector,'vtpl_xpath', 0, 11) == 0) return substr($selector, 12, -1);

            [964] => 

            [965] => 	$selector = (string) $selector;

            [966] => 

            [967] => 	//convert , to | union operator to allow multiple queries

            [968] => 	$selector = str_replace(',', '|', $selector);

            [969] => 

            [970] => 	$cssSelector = [

            [971] => 		// E > F: Matches any F element that is a child of an element E

            [972] => 		'/\s*>\s*/',

            [973] => 

            [974] => 		// E + F: Matches any F element immediately preceded by an element

            [975] => 		'/\s*\+\s*/',

            [976] => 

            [977] => 		// E F: Matches any F element that is a descendant of an E element

            [978] => 		'/([a-zA-Z\*="\[\]#._-])\s+([a-zA-Z\*="\[\]#._-])/', //'/([a-zA-Z\*="\[\]#._-])\s+([a-zA-Z\*#._-])/',

            [979] => 

            [980] => 		// E:first-child: Matches element E when E is the first child of its parent

            [981] => 		'/([a-z#\.]\w*):first-child/',

            [982] => 

            [983] => 		// E:nth-child() Matches the nth child element

            [984] => 		'/([a-z#\.]\w*):nth-child\((\d+)\)/',

            [985] => 

            [986] => 		// E:first: Matches the first element from the set

            [987] => 		'/([a-z#\.]\w*):first/',

            [988] => 

            [989] => 		// E:nth(2): Matches the nth element from the set

            [990] => 		'/([a-z#\.]\w*):nth\((\d+)\)/',

            [991] => 

            [992] => 		// E[foo="warning"]: Matches any E element whose "foo" attribute value is exactly equal to "warning"

            [993] => 		'/([a-z]\w*)\[([a-z][\w\-_]*)\="([^"]*)"]/',

            [994] => 

            [995] => 		// E[foo]: Matches any E element with the "foo" attribute set (whatever the value)

            [996] => 		'/([a-z]\w*)\[([a-z][\w_\-]*)\]/',

            [997] => 

            [998] => 		// E[!foo]: Matches any E element without the "foo" attribute set

            [999] => 		'/([a-z]\w*)\[!([a-z][\w\-_]*)\]/',

            [1000] => 

            [1001] => 		// [foo="warning"]: Matches any element whose "foo" attribute value is exactly equal to "warning"

            [1002] => 		'/\[([a-z][\w\-_]*)\=\"(.*)\"\]/',

            [1003] => 

            [1004] => 		// [foo*="warning"]: Matches any element whose "foo" attribute value contains the string "warning" and has other attributes

            [1005] => 		'/(?<=\])\[([a-z][\w\-_]*)\*\=\"([^"]+)\"\]/',

            [1006] => 

            [1007] => 		// [foo*="warning"]: Matches any element whose "foo" attribute value contains the string "warning"

            [1008] => 		'/\[([a-z][\w\-_]*)\*\=\"([^"]+)\"\]/',

            [1009] => 

            [1010] => 		// [foo^="warning"]: Matches any element whose "foo" attribute value begins with the string "warning"

            [1011] => 		'/\[([a-z][\w_\-]*)\^\=\"([^"]+)\"\]/',

            [1012] => 

            [1013] => 		// [foo$="warning"]: Matches any element whose "foo" attribute value ends  with the string "warning"

            [1014] => 		'/\[([a-z][\w_\-]*)\$\=\"([^"]+)\"\]/',

            [1015] => 

            [1016] => 		// [foo][baz]: Matches any element with the "foo" attribute set (whatever the value)

            [1017] => 		'/(?<=\])(? 

            [1019] => 		// [foo]: Matches any element with the "foo" attribute set (whatever the value)

            [1020] => 		'/\[([a-z][\w_\-]*)\]/',

            [1021] => 

            [1022] => 		// element[foo*]: Matches any element that starts with "foo" attribute (whatever the value)

            [1023] => 		'/(\w+)\[([a-z][\w\-]*)\*\]/',

            [1024] => 

            [1025] => 		// [foo*]: Matches any element that starts with "foo" attribute (whatever the value) and has other attributes

            [1026] => 		'/(?<=\])\[([a-z][\w\-]*)\*\]/',

            [1027] => 

            [1028] => 		// [foo*]: Matches any element that starts with "foo" attribute (whatever the value) and is a single attribute

            [1029] => 		'/(? 

            [1031] => 		// div.warn*: HTML only. The same as DIV[class*="warning"]

            [1032] => 		'/([a-z]\w*|\*)\.([a-z][\w\-_]*)\*/',

            [1033] => 

            [1034] => 		// div.warning: HTML only. The same as DIV[class~="warning"]

            [1035] => 		'/([a-z]\w*|\*)\.([a-z][\w\-_]*)+/',

            [1036] => 

            [1037] => 		// .warn*: HTML only. The same as [class*="warning"]

            [1038] => 		'/\.([a-z][\w\-\_]*)\*/',

            [1039] => 

            [1040] => 		// .warning: HTML only. The same as [class~="warning"]

            [1041] => 		'/\.([a-z][\w\-\_]*)+/',

            [1042] => 

            [1043] => 		// E#myid: Matches any E element with id-attribute equal to "myid"

            [1044] => 		'/([a-z]\w*)\#([a-z][\w\-_]*)/',

            [1045] => 

            [1046] => 		// #myid: Matches any E element with id-attribute equal to "myid"

            [1047] => 		'/\#([a-z][\w\-_]*)/',

            [1048] => 	];

            [1049] => 

            [1050] => 	$xpathQuery = [

            [1051] => 		'/', //element > child

            [1052] => 		'/following-sibling::*[1]/self::', // element + precedent

            [1053] => 		'\1//\2', //element descendent

            [1054] => 		'*[1]/self::\1', //element:first-child

            [1055] => 		'*[\2]/self::\1', //element:nth-child(2)

            [1056] => 		'\1[1]', //element:first

            [1057] => 		'\1[\2]', //element:nth(2)

            [1058] => 		'\1[ contains( concat( " ", @\2, " " ), concat( " ", "\3", " " ) ) ]', //element[attribute="string"]

            [1059] => 		'\1 [ @\2 ]', //element[attribute]

            [1060] => 		'\1 [ not(@\2) ]', //element[!attribute]

            [1061] => 		'[ contains( concat( " ", @\1, " " ), concat( " ", "\2", " " ) ) ]', //[foo="warning"]

            [1062] => 		'*[ contains( concat( " ", @\1, " " ), concat( " ", "\2", " " ) ) ]', //[foo="warning"]

            [1063] => 		'*[ contains( concat( " ", @\1, " " ), "\2" ) ]', //[foo*="warning"]

            [1064] => 		'*[ contains( concat( " ", @\1, " " ), concat( " ", "\2", " " ) ) ]', //[foo^="warning"] not implemented

            [1065] => 		'*[ contains( concat( " ", @\1, " " ), concat( " ", "\2", " " ) ) ]', //[foo$="warning"] not implemented

            [1066] => 		'[ @\1 ]', //[attribute][attribute]

            [1067] => 		'*[ @\1 ]', //[attribute]

            [1068] => 		'\1 [ @*[starts-with(name(), "\2")] ]', //element[attr*]

            [1069] => 		'[ @*[starts-with(name(), "\1")] ]', //[attr*] - attribute with other attributes

            [1070] => 		'*[ @*[starts-with(name(), "\1")] ]', //[attr*] - single attribute

            [1071] => 		'\1[ contains( concat( " ", @class, " " ), concat( " ", "\2") ) ]', //element[class*="string"]

            [1072] => 		'\1[ contains( concat( " ", @class, " " ), concat( " ", "\2", " " ) ) ]', //element[class~="string"]

            [1073] => 		'*[ contains( concat( " ", @class, " " ), concat( " ", "\1") ) ]', //[class*="string"]

            [1074] => 		'*[ contains( concat( " ", @class, " " ), concat( " ", "\1", " " ) ) ]', //element[class~="string"]

            [1075] => 		'\1[ @id = "\2" ]', //element#id

            [1076] => 		'*[ @id = "\1" ]', //#id

            [1077] => 	];

            [1078] => 

            [1079] => 	$result = (string) '//' . preg_replace($cssSelector, $xpathQuery, $selector);

            [1080] => 

            [1081] => 	return $result;

            [1082] => }

            [1083] => 

            [1084] => /**

            [1085] =>  * Imports an HTML string into a DOM document and returns the node.

            [1086] =>  *

            [1087] =>  * @param string      $html     The HTML string to import.

            [1088] =>  * @param string|null $selector An optional CSS selector to find a specific node.

            [1089] =>  *

            [1090] =>  * @return \DOMNode|null The imported DOM node or null.

            [1091] =>  */

            [1092] => function importHtml(string $html, ?string $selector = null): ?\DOMNode {

            [1093] => 	$dom = new \DomDocument();

            [1094] => 	$dom->preserveWhiteSpace  = true;

            [1095] => 	$dom->recover             = true;

            [1096] => 	$dom->strictErrorChecking = false;

            [1097] => 	$dom->substituteEntities  = false;

            [1098] => 	$dom->formatOutput        = false;

            [1099] => 	$dom->resolveExternals    = false;

            [1100] => 	$dom->validateOnParse     = false;

            [1101] => 	$dom->xmlStandalone       = true;

            [1102] => 	$dom->loadHTML($html);

            [1103] => 

            [1104] => 	if ($selector) {

            [1105] => 		$xpath = new \DOMXpath($dom);

            [1106] => 		$xpathQuery = cssToXpath($selector);

            [1107] => 		$result = $xpath->query($xpathQuery);

            [1108] => 

            [1109] => 		$nodeToImport = $result->item(0)?->firstChild;

            [1110] => 	} else {

            [1111] => 		$body         = $dom->getElementsByTagName('body');

            [1112] => 		$nodeToImport = $body->item(0)?->firstChild;

            [1113] => 	}

            [1114] => 

            [1115] => 	return $nodeToImport;

            [1116] => }

            [1117] => 

            [1118] => /**

            [1119] =>  * Converts a dash-separated string to CamelCase.

            [1120] =>  *

            [1121] =>  * @param string $string The string to convert.

            [1122] =>  * @param string $dash   The dash character used as separator.

            [1123] =>  *

            [1124] =>  * @return string The CamelCase string.

            [1125] =>  */

            [1126] => function dashesToCamelCase(string $string, string $dash = '-'): string {

            [1127] => 	return str_replace($dash, '', ucwords($string, $dash));

            [1128] => }

            [1129] => 

            [1130] => /**

            [1131] =>  * Recursively converts underscore-separated array keys to camelCase.

            [1132] =>  *

            [1133] =>  * @param array $array The array whose keys to convert.

            [1134] =>  *

            [1135] =>  * @return array The array with converted keys.

            [1136] =>  */

            [1137] => function arrayKeysToCamelCase(array &$array): array {

            [1138] => 	foreach ($array as $key => &$value) {

            [1139] => 		if (is_array($value)) {

            [1140] => 			$value = arrayKeysToCamelCase($value);

            [1141] => 		}

            [1142] => 

            [1143] => 		$newKey = lcfirst(dashesToCamelCase($key, '_'));

            [1144] => 

            [1145] => 		if ($newKey != $key) {

            [1146] => 			$array[$newKey] = $value;

            [1147] => 			unset($array[$key]);

            [1148] => 		}

            [1149] => 	}

            [1150] => 

            [1151] => 	return $array;

            [1152] => }

            [1153] => 

            [1154] => /**

            [1155] =>  * Removes extra spaces from a string.

            [1156] =>  *

            [1157] =>  * @param string $string The string to process.

            [1158] =>  *

            [1159] =>  * @return string The string with extra spaces removed.

            [1160] =>  */

            [1161] => function stripExtraSpaces(string $string): string {

            [1162] => 	foreach (['\t', '\n', '\r', ' '] as $space) {

            [1163] => 		$string = preg_replace('/(' . $space . ')' . $space . '+/', '\1', $string);

            [1164] => 	}

            [1165] => 

            [1166] => 	return $string;

            [1167] => }

            [1168] => 

            [1169] => /**

            [1170] =>  * Reads the last N lines from a file.

            [1171] =>  *

            [1172] =>  * @param string $filename The path to the file.

            [1173] =>  * @param int    $lines    The number of lines to read from the end.

            [1174] =>  *

            [1175] =>  * @return string|false The last lines of the file, or false on failure.

            [1176] =>  */

            [1177] => function tail(string $filename, int $lines = 1000): string|false {

            [1178] => 	$file = @fopen($filename, 'rb');

            [1179] => 

            [1180] => 	if ($file === false) {

            [1181] => 		return false;

            [1182] => 	}

            [1183] => 

            [1184] => 	$buffer = 4096;

            [1185] => 	$output = '';

            [1186] => 	$chunk  = '';

            [1187] => 

            [1188] => 	@fseek($file, -1, SEEK_END);

            [1189] => 

            [1190] => 	if (fread($file, 1) != "\n") {

            [1191] => 		$lines -= 1;

            [1192] => 	}

            [1193] => 

            [1194] => 	while (ftell($file) > 0 && $lines >= 0) {

            [1195] => 		$seek = min(ftell($file), $buffer);

            [1196] => 		fseek($file, -$seek, SEEK_CUR);

            [1197] => 		$output = ($chunk = fread($file, $seek)) . $output;

            [1198] => 		//fseek($file, -mb_strlen($chunk, '8bit'), SEEK_CUR);

            [1199] => 		fseek($file, strlen($chunk), SEEK_CUR);

            [1200] => 		$lines -= substr_count($chunk, "\n");

            [1201] => 	}

            [1202] => 

            [1203] => 	while ($lines++ < 0) {

            [1204] => 		$output = substr($output, strpos($output, "\n") + 1);

            [1205] => 	}

            [1206] => 

            [1207] => 	fclose($file);

            [1208] => 

            [1209] => 	return trim($output);

            [1210] => }

            [1211] => 

            [1212] => /**

            [1213] =>  * Return current module name.

            [1214] =>  *

            [1215] =>  * @return string

            [1216] =>  */

            [1217] => function getModuleName(): string {

            [1218] => 	return strtolower(\Vvveb\System\Core\FrontController::getModuleName());

            [1219] => }

            [1220] => 

            [1221] => /**

            [1222] =>  * Return current action name.

            [1223] =>  *

            [1224] =>  * @return string

            [1225] =>  */

            [1226] => function getActionName(): string {

            [1227] => 	return strtolower(\Vvveb\System\Core\FrontController::getActionName());

            [1228] => }

            [1229] => 

            [1230] => /**

            [1231] =>  * Inserts a new key/value before the key in the array.

            [1232] =>  *

            [1233] =>  * @param string $key       The key to insert before.

            [1234] =>  * @param array  $array     An array to insert in to.

            [1235] =>  * @param string $new_key   The key to insert.

            [1236] =>  * @param mixed  $new_value An value to insert.

            [1237] =>  *

            [1238] =>  * @return array The new array if the key exists, otherwise the unchanged array.

            [1239] =>  *

            [1240] =>  * @see arrayInsertAfter()

            [1241] =>  */

            [1242] => function arrayInsertBefore(string $key, array &$array, string $new_key, mixed $new_value): array {

            [1243] => 	if (array_key_exists($key, $array)) {

            [1244] => 		$new = [];

            [1245] => 

            [1246] => 		foreach ($array as $k => $value) {

            [1247] => 			if ($k === $key) {

            [1248] => 				$new[$new_key] = $new_value;

            [1249] => 			}

            [1250] => 			$new[$k] = $value;

            [1251] => 		}

            [1252] => 

            [1253] => 		return $new;

            [1254] => 	}

            [1255] => 

            [1256] => 	return $array;

            [1257] => }

            [1258] => 

            [1259] => /**

            [1260] =>  * Inserts a new array before the key in the array.

            [1261] =>  *

            [1262] =>  * @param string $key       The key to insert before.

            [1263] =>  * @param array  $array     An array to insert in to.

            [1264] =>  * @param array  $new_array An array to insert.

            [1265] =>  *

            [1266] =>  * @return array The new array if the key exists, otherwise the unchanged array.

            [1267] =>  *

            [1268] =>  * @see arrayInsertAfter()

            [1269] =>  */

            [1270] => function arrayInsertArrayBefore(string $key, array &$array, array $new_array): array {

            [1271] => 	if (array_key_exists($key, $array)) {

            [1272] => 		$new = [];

            [1273] => 

            [1274] => 		foreach ($array as $k => $value) {

            [1275] => 			if ($k === $key) {

            [1276] => 				$new += $new_array;

            [1277] => 			}

            [1278] => 			$new[$k] = $value;

            [1279] => 		}

            [1280] => 

            [1281] => 		return $new;

            [1282] => 	}

            [1283] => 

            [1284] => 	return $array;

            [1285] => }

            [1286] => 

            [1287] => /**

            [1288] =>  * Inserts a new key/value after the key in the array.

            [1289] =>  *

            [1290] =>  * @param string $key       The key to insert after.

            [1291] =>  * @param array  $array     An array to insert in to.

            [1292] =>  * @param string $new_key   The key to insert.

            [1293] =>  * @param mixed  $new_value An value to insert.

            [1294] =>  *

            [1295] =>  * @return array The new array if the key exists, otherwise the unchanged array.

            [1296] =>  *

            [1297] =>  * @see arrayInsertBefore()

            [1298] =>  */

            [1299] => function arrayInsertAfter(string $key, array &$array, string $new_key, mixed $new_value): array {

            [1300] => 	if (array_key_exists($key, $array)) {

            [1301] => 		$new = [];

            [1302] => 

            [1303] => 		foreach ($array as $k => $value) {

            [1304] => 			$new[$k] = $value;

            [1305] => 

            [1306] => 			if ($k === $key) {

            [1307] => 				$new[$new_key] = $new_value;

            [1308] => 			}

            [1309] => 		}

            [1310] => 

            [1311] => 		return $new;

            [1312] => 	}

            [1313] => 

            [1314] => 	return $array;

            [1315] => }

            [1316] => 

            [1317] => /**

            [1318] =>  * Inserts a new array after the key in the array.

            [1319] =>  *

            [1320] =>  * @param string $key       The key to insert after.

            [1321] =>  * @param array  $array     An array to insert in to.

            [1322] =>  * @param array  $new_array An array to insert.

            [1323] =>  *

            [1324] =>  * @return array The new array if the key exists, otherwise the unchanged array.

            [1325] =>  *

            [1326] =>  * @see arrayInsertBefore()

            [1327] =>  */

            [1328] => function arrayInsertArrayAfter(string $key, array &$array, array $new_array): array {

            [1329] => 	if (array_key_exists($key, $array)) {

            [1330] => 		$new = [];

            [1331] => 

            [1332] => 		foreach ($array as $k => $value) {

            [1333] => 			$new[$k] = $value;

            [1334] => 

            [1335] => 			if ($k === $key) {

            [1336] => 				$new += $new_array;

            [1337] => 			}

            [1338] => 		}

            [1339] => 

            [1340] => 		return $new;

            [1341] => 	}

            [1342] => 

            [1343] => 	return $array;

            [1344] => }

            [1345] => 

            [1346] => /**

            [1347] =>  * Get a request parameter value.

            [1348] =>  *

            [1349] =>  * @param string $key The request parameter key.

            [1350] =>  *

            [1351] =>  * @return mixed The parameter value, or false if not set.

            [1352] =>  */

            [1353] => function get(string $key): mixed {

            [1354] => 	return System\Core\Request::getInstance()->get[$key] ?? false;

            [1355] => }

            [1356] => 

            [1357] => /**

            [1358] =>  * Check if the page is loaded in the editor.

            [1359] =>  *

            [1360] =>  * @return boolean

            [1361] =>  */

            [1362] => function isEditor(): bool {

            [1363] => 	return isset($_GET['r']);

            [1364] => }

            [1365] => 

            [1366] => /**

            [1367] =>  * Log an error message.

            [1368] =>  *

            [1369] =>  * @param string $message The error message to log.

            [1370] =>  *

            [1371] =>  * @return void

            [1372] =>  */

            [1373] => function logError(string $message): void {

            [1374] => 	error_log($message);

            [1375] => }

            [1376] => 

            [1377] => /**

            [1378] =>  * Return list of theme folders.

            [1379] =>  *

            [1380] =>  * @param string|false $theme The theme name, or false to use the current theme.

            [1381] =>  *

            [1382] =>  * @return array The list of theme folders.

            [1383] =>  */

            [1384] => function getThemeFolderList(string|false $theme = false): array {

            [1385] => 	$skipFolders    = ['src', 'source', 'backup', 'sections', 'blocks', 'inputs', 'css', 'scss', 'fonts', 'img', 'import', 'node_modules', 'screenshots', 'video', 'js'];

            [1386] => 

            [1387] => 	if (! $theme) {

            [1388] => 		$theme = \Vvveb\System\Sites::getTheme(SITE_ID) ?? 'default';

            [1389] => 	}

            [1390] => 

            [1391] => 	$themeFolder = DIR_THEMES . $theme;

            [1392] => 	$files       = glob("$themeFolder/*", GLOB_ONLYDIR) ?: [];

            [1393] => 	$pages['/']  = ['name' => '/', 'title' => '/', 'filename' => '/', 'file' => '/', 'path' => '/', 'folder' => $theme];

            [1394] => 

            [1395] => 	foreach ($files as $file) {

            [1396] => 		$folder     = preg_replace('@^.*/themes/[^/]+/@', '', $file);

            [1397] => 		$filename   = basename($file);

            [1398] => 

            [1399] => 		if (in_array($folder, $skipFolders)) {

            [1400] => 			continue;

            [1401] => 		}

            [1402] => 

            [1403] => 		$pages[$file]  = ['name' => $filename, 'title' => "/$filename", 'filename' => $filename, 'file' => $filename, 'folder' => $theme];

            [1404] => 	}

            [1405] => 

            [1406] => 	return $pages;

            [1407] => }

            [1408] => 

            [1409] => /**

            [1410] =>  * Return all available field type definitions.

            [1411] =>  *

            [1412] =>  * @return array Associative array of field types grouped by category.

            [1413] =>  */

            [1414] => function getFieldTypes(): array {

            [1415] => 	return

            [1416] => 	[

            [1417] => 		'basic' => [

            [1418] => 			'text' => [

            [1419] => 				'name' => __('Text'),

            [1420] => 				'icon' => 'icon-text-outline',

            [1421] => 			],

            [1422] => 			'textarea' => [

            [1423] => 				'name' => __('Textarea'),

            [1424] => 				'icon' => 'icon-document-text-outline',

            [1425] => 			],

            [1426] => 			'number' => [

            [1427] => 				'name' => __('Number'),

            [1428] => 				'icon' => 'icon-document-text-outline',

            [1429] => 			],

            [1430] => 			'range' => [

            [1431] => 				'name' => __('Range'),

            [1432] => 				'icon' => 'icon-git-commit-outline',

            [1433] => 			],

            [1434] => 			'email' => [

            [1435] => 				'name' => __('Email'),

            [1436] => 				'icon' => 'icon-mail-outline',

            [1437] => 			],

            [1438] => 			'url' => [

            [1439] => 				'name' => __('Url'),

            [1440] => 				'icon' => 'icon-link-outline',

            [1441] => 			],

            [1442] => 			'tel' => [

            [1443] => 				'name' => __('Telephone'),

            [1444] => 				'icon' => 'icon-call-outline',

            [1445] => 			],

            [1446] => 			'password' => [

            [1447] => 				'name' => __('Password'),

            [1448] => 				'icon' => 'icon-lock-open-outline',

            [1449] => 			],

            [1450] => 			'select' => [

            [1451] => 				'name' => __('Select'),

            [1452] => 				'icon' => 'icon-chevron-down-circle-outline',

            [1453] => 			],

            [1454] => 		],

            [1455] => 		'content' => [

            [1456] => 			'image'   => [

            [1457] => 				'name' => __('Image'),

            [1458] => 				'icon' => 'icon-image-outline',

            [1459] => 			],

            [1460] => 			'file'    => [

            [1461] => 				'name' => __('File'),

            [1462] => 				'icon' => 'icon-document-outline',

            [1463] => 			],

            [1464] => 			'wysiwyg' => [

            [1465] => 				'name' => __('Wysiwyg Editor'),

            [1466] => 				'icon' => 'icon-create-outline',

            [1467] => 			],

            [1468] => 			'oembed'  => [

            [1469] => 				'name' => __('oEmbed'),

            [1470] => 				'icon' => 'icon-videocam-outline',

            [1471] => 			],

            [1472] => 			'gallery'    => [

            [1473] => 				'name' => __('Gallery'),

            [1474] => 				'icon' => 'icon-images-outline',

            [1475] => 			],

            [1476] => 		],

            [1477] => 		'choice' => [

            [1478] => 			'select' => [

            [1479] => 				'name' => __('Select'),

            [1480] => 				'icon' => 'icon-chevron-down-circle-outline',

            [1481] => 			],

            [1482] => 			'checkbox' => [

            [1483] => 				'name' => __('Checkbox'),

            [1484] => 				'icon' => 'icon-checkbox-outline',

            [1485] => 			],

            [1486] => 			'radio' => [

            [1487] => 				'name' => __('Radio Button'),

            [1488] => 				'icon' => 'icon-checkmark-circle-outline',

            [1489] => 			],

            [1490] => 			'toggle' => [

            [1491] => 				'name' => __('Toggle'),

            [1492] => 				'icon' => 'icon-toggle-outline',

            [1493] => 			],

            [1494] => 		],

            [1495] => 		'relational' => [

            [1496] => 			'link' => [

            [1497] => 				'name' => __('Link'),

            [1498] => 				'icon' => 'icon-link-outline',

            [1499] => 			],

            [1500] => 			'post' => [

            [1501] => 				'name' => __('Post'),

            [1502] => 				'icon' => 'icon-document-text-outline',

            [1503] => 			],

            [1504] => 			'product' => [

            [1505] => 				'name' => __('Product'),

            [1506] => 				'icon' => 'icon-cube-outline',

            [1507] => 			],

            [1508] => 			'relationship' => [

            [1509] => 				'name' => __('Relationship'),

            [1510] => 				'icon' => 'icon-link-outline',

            [1511] => 			],

            [1512] => 			'taxonomy' => [

            [1513] => 				'name' => __('Taxonomy'),

            [1514] => 				'icon' => 'icon-apps-outline',

            [1515] => 			],

            [1516] => 			'user' => [

            [1517] => 				'name' => __('User'),

            [1518] => 				'icon' => 'icon-person-outline',

            [1519] => 			],

            [1520] => 		],

            [1521] => 		'widgets' => [

            [1522] => 			'google_map' => [

            [1523] => 				'name' => __('Google Map'),

            [1524] => 				'icon' => 'icon-map-outline',

            [1525] => 			],

            [1526] => 			'color' => [

            [1527] => 				'name' => __('Color'),

            [1528] => 				'icon' => 'icon-color-palette-outline',

            [1529] => 			],

            [1530] => 			'time' => [

            [1531] => 				'name' => __('Time'),

            [1532] => 				'icon' => 'icon-time-outline',

            [1533] => 			],

            [1534] => 			'date' => [

            [1535] => 				'name' => __('Date'),

            [1536] => 				'icon' => 'icon-today-outline',

            [1537] => 			],

            [1538] => 			'date_time' => [

            [1539] => 				'name' => __('Date time'),

            [1540] => 				'icon' => 'icon-calendar-clear-outline',

            [1541] => 			],

            [1542] => 			'month' => [

            [1543] => 				'name' => __('Month'),

            [1544] => 				'icon' => 'icon-calendar-outline',

            [1545] => 			],

            [1546] => 			'week' => [

            [1547] => 				'name' => __('week'),

            [1548] => 				'icon' => 'icon-calendar-number-outline',

            [1549] => 			],

            [1550] => 		],

            [1551] => 		'advanced' => [

            [1552] => 			'message' => [

            [1553] => 				'name' => __('Message'),

            [1554] => 				'icon' => 'icon-chevron-down-circle-outline',

            [1555] => 			],

            [1556] => 			'accordion' => [

            [1557] => 				'name' => __('Accordion'),

            [1558] => 				'icon' => 'icon-chevron-down-circle-outline',

            [1559] => 			],

            [1560] => 			'tab' => [

            [1561] => 				'name' => __('Tab'),

            [1562] => 				'icon' => 'icon-chevron-down-circle-outline',

            [1563] => 			],

            [1564] => 		],

            [1565] => 	];

            [1566] => }

            [1567] => 

            [1568] => /**

            [1569] =>  * Return list of default template filenames.

            [1570] =>  *

            [1571] =>  * @return array List of default template filenames.

            [1572] =>  */

            [1573] => function getDefaultTemplateList(): array {

            [1574] => 	return [

            [1575] => 		'index.html',

            [1576] => 		'index.coming-soon.html',

            [1577] => 		'index.maintenance.html',

            [1578] => 		'blank.html',

            [1579] => 		'error404.html',

            [1580] => 		'error500.html',

            [1581] => 		'contact.html',

            [1582] => 		'search/index.html',

            [1583] => 		'content/post.html',

            [1584] => 		'content/page.html',

            [1585] => 		'content/index.html',

            [1586] => 		'content/category.html',

            [1587] => 		'content/tag.html',

            [1588] => 		'product/index.html',

            [1589] => 		'product/product.html',

            [1590] => 		'product/category.html',

            [1591] => 	];

            [1592] => }

            [1593] => 

            [1594] => /**

            [1595] =>  * Return list of available templates for a theme.

            [1596] =>  *

            [1597] =>  * @param string|null $theme The theme name, or null to use the current theme.

            [1598] =>  * @param array       $skip  List of folder names to skip.

            [1599] =>  *

            [1600] =>  * @return array Associative array of templates.

            [1601] =>  */

            [1602] => function getTemplateList(?string $theme = null, array $skip = []): array {

            [1603] => 	$friendlyNames =  [

            [1604] => 		'index'                     => ['name' =>  __('Home page'), 'description'         =>  __('Website homepage'), 'global' => true],

            [1605] => 		'index.coming-soon'         => ['name' =>  __('Coming soon'), 'description'       =>  __('Coming soon message page'), 'global' => true],

            [1606] => 		'index.maintenance'         => ['name' =>  __('Under maintenance'), 'description' =>  __('Website under maintenance message page'), 'global' => true],

            [1607] => 		'contact'                   => ['name' =>  __('Contact us page'), 'description'   =>  __('Contact us page')],

            [1608] => 		'blank'                     => ['name' =>  __('Blank page'), 'description'        =>  __('Template page used for new pages')],

            [1609] => 		'product'                   => ['name' =>  __('Product page'), 'description'      =>  __('Used to display a product'), 'editor' => ['template' => 'product'], 'global' => true],

            [1610] => 		'error404'                  => ['name' =>  __('Page not found'), 'description'    =>  __('Shows when a page is not available'), 'global' => true],

            [1611] => 		'error500'                  => ['name' =>  __('Server error'), 'description'      =>  __('Site error display page'), 'global' => true],

            [1612] => 		'content-index'             => ['name' =>  __('Blog homepage'), 'description'     =>  __('Blog page with latest posts'), 'global' => true],

            [1613] => 		'content-post'              => ['name' =>  __('Blog post'), 'description'         =>  __('Blog post'), 'editor' => ['template' => 'post'], 'global' => true],

            [1614] => 		'content-page'              => ['name' =>  __('Page'), 'description'         =>  __('Page'), 'editor' => ['template' => 'page'], 'global' => true],

            [1615] => 		'product-index'             => ['name' =>  __('Shop page'), 'description'         =>  __('Shop homepage'), 'global' => true],

            [1616] => 		'search-index'              => ['name' =>  __('Search page'), 'description'       =>  __('Search page'), 'global' => true],

            [1617] => 		'user-index'                => ['name' =>  __('Dashboard'), 'description'         =>  __('User dashboard'), 'global' => true],

            [1618] => 	];

            [1619] => 

            [1620] => 	$skipFolders    = array_merge(['src', 'source', 'backup', 'sections', 'blocks', 'inputs', 'css', 'scss', 'screenshots', 'locale', 'node_modules'], $skip);

            [1621] => 

            [1622] => 	if (! $theme) {

            [1623] => 		$theme = \Vvveb\System\Sites::getTheme(SITE_ID) ?? 'default';

            [1624] => 	}

            [1625] => 	$pages       = [];

            [1626] => 	$themeFolder = DIR_THEMES . $theme;

            [1627] => 	//$files       = glob("$themeFolder/{,*/*/,*/}*.html", GLOB_BRACE);

            [1628] => 	$glob        = ['', '*/*/', '*/'];

            [1629] => 	$files       = globBrace($themeFolder . DS, $glob, '*.html');

            [1630] => 

            [1631] => 	foreach ($files as $file) {

            [1632] => 		$file     = preg_replace('@^.*[\\\/]themes[\\\/][^\\\/]+[\\\/]@', '', $file);

            [1633] => 		$filename = basename($file);

            [1634] => 

            [1635] => 		$folder   = \Vvveb\System\Functions\Str::match('@(\w+)/.*?$@', $file) ?? DS;

            [1636] => 		$path     = \Vvveb\System\Functions\Str::match('@(\w+)/.*?$@', $file);

            [1637] => 

            [1638] => 		if (in_array($folder, $skipFolders)) {

            [1639] => 			continue;

            [1640] => 		}

            [1641] => 		$name        = $title       = str_replace('.html', '', $filename);

            [1642] => 		$description = '';

            [1643] => 		$name        = (! empty($folder) && $folder != DS) ? "$folder-$name" : $name;

            [1644] => 

            [1645] => 		if (isset($friendlyNames[$name])) {

            [1646] => 			if (isset($friendlyNames[$name]['description'])) {

            [1647] => 				$description = $friendlyNames[$name]['description'];

            [1648] => 			}

            [1649] => 

            [1650] => 			$title = $friendlyNames[$name]['name'];

            [1651] => 		}

            [1652] => 

            [1653] => 		$url = PUBLIC_PATH . "themes/$theme/$file";

            [1654] => 		$title = str_replace('index', 'home', $title);

            [1655] => 

            [1656] => 		$pages[$name]  = ['name' => $name, 'filename' => $filename, 'file' => $file, 'url' => $url, 'title' => humanReadable($title), 'folder' => $path, 'description' => $description, 'global' => $friendlyNames[$name]['global'] ?? false];

            [1657] => 

            [1658] => 		if (isset($friendlyNames[$name]['editor'])) {

            [1659] => 			$pages[$name]['editor'] = $friendlyNames[$name]['editor'];

            [1660] => 		}

            [1661] => 	}

            [1662] => 

            [1663] => 	//ksort($pages);

            [1664] => 

            [1665] => 	return $pages;

            [1666] => }

            [1667] => 

            [1668] => /**

            [1669] =>  * Sanitize filename by removing dangerous characters.

            [1670] =>  *

            [1671] =>  * @param string $file          The filename to sanitize.

            [1672] =>  * @param bool   $normalizePath Whether to normalize directory separators to the OS separator.

            [1673] =>  *

            [1674] =>  * @return string The sanitized filename.

            [1675] =>  */

            [1676] => function sanitizeFileName(string $file, bool $normalizePath = true): string {

            [1677] => 	if (! $file) {

            [1678] => 		return $file;

            [1679] => 	}

            [1680] => 

            [1681] => 	//remove null bytes

            [1682] => 	$file = str_replace(chr(0), '', $file);

            [1683] => 	//sanitize, remove double dot .. and remove get parameters if any

            [1684] => 	$prev = '';

            [1685] => 	while ($prev != $file) {

            [1686] => 		$prev = $file;

            [1687] => 		$file = preg_replace('@\?.*$|\.{2,}|[^\w\-\./: \\\]@' , '', $file);

            [1688] => 	}

            [1689] => 	//replace directory separators with OS specific separator

            [1690] => 	if ($normalizePath) {

            [1691] => 		$file = str_replace(['\\', '/'], DS, $file);

            [1692] => 	}

            [1693] => 

            [1694] => 	return $file;

            [1695] => }

            [1696] => 

            [1697] => /**

            [1698] =>  * Format byte count to human-readable string (KB, MB, etc.).

            [1699] =>  *

            [1700] =>  * @param int $bytes The number of bytes.

            [1701] =>  *

            [1702] =>  * @return string The formatted string with appropriate unit.

            [1703] =>  */

            [1704] => function formatBytes(int $bytes): string {

            [1705] => 	$i     = 0;

            [1706] => 	$units = ['', 'K', 'M', 'G', 'T', 'P', 'E'];

            [1707] => 	$bytes = abs($bytes);

            [1708] => 

            [1709] => 	while (($remainder = ($bytes / 1024)) > 1) {

            [1710] => 		$bytes = $remainder;

            [1711] => 		$i++;

            [1712] => 	}

            [1713] => 

            [1714] => 	return round($bytes, 2) . ' ' . $units[$i] . 'B';

            [1715] => }

            [1716] => 

            [1717] => /**

            [1718] =>  * Parse quantity string with K/M/G suffix to integer bytes.

            [1719] =>  *

            [1720] =>  * @param string $string The quantity string to parse (e.g. "10M", "512K").

            [1721] =>  *

            [1722] =>  * @return int The parsed integer value in bytes.

            [1723] =>  */

            [1724] => function parseQuantity(string $string): int {

            [1725] => 	if (function_exists('\ini_parse_quantity')) {

            [1726] => 		return \ini_parse_quantity($string);

            [1727] => 	}

            [1728] => 

            [1729] => 	$string     = trim($string);

            [1730] => 	$multiplier = lcfirst($string[strlen($string) - 1]);

            [1731] => 	$number     = intval($string);

            [1732] => 

            [1733] => 	return match ($multiplier) {

            [1734] => 		'k' => $number * 1024,

            [1735] => 		'm' => $number * 1048576,

            [1736] => 		'g' => $number * 1073741824,

            [1737] => 		default => $number,

            [1738] => 	};

            [1739] => }

            [1740] => 

            [1741] => /**

            [1742] =>  * Check if a controller file exists.

            [1743] =>  *

            [1744] =>  * @param string $name The controller name.

            [1745] =>  * @param string $app  The application directory name.

            [1746] =>  *

            [1747] =>  * @return bool True if the controller file exists.

            [1748] =>  */

            [1749] => function isController(string $name, string $app = APP): bool {

            [1750] => 	$file   = DIR_ROOT . $app . DS . 'controller' . DS . strtolower($name) . '.php';

            [1751] => 	$exists = file_exists($file);

            [1752] => 

            [1753] => 	return $exists;

            [1754] => }

            [1755] => 

            [1756] => /**

            [1757] =>  * Check if a model SQL file exists.

            [1758] =>  *

            [1759] =>  * @param string $name The model name.

            [1760] =>  * @param string $app  The application directory name.

            [1761] =>  *

            [1762] =>  * @return bool True if the model SQL file exists.

            [1763] =>  */

            [1764] => function isModel(string $name, string $app = APP): bool {

            [1765] => 	$file   = DIR_ROOT . $app . DS . 'sql' . DS . DB_ENGINE . DS . $name . '.sql';

            [1766] => 	$exists = file_exists($file);

            [1767] => 

            [1768] => 	return $exists;

            [1769] => }

            [1770] => 

            [1771] => /**

            [1772] =>  * Instantiate and return a model object by name.

            [1773] =>  *

            [1774] =>  * @param string $model The model name.

            [1775] =>  *

            [1776] =>  * @return object The instantiated model object.

            [1777] =>  */

            [1778] => function model(string $model): object {

            [1779] => 	$modelClass = 'Vvveb\Sql\\' . ucwords($model) . 'SQL';

            [1780] => 

            [1781] => 	return new $modelClass();

            [1782] => }

            [1783] => 

            [1784] => /**

            [1785] =>  * Instantiate and return a controller object by name.

            [1786] =>  *

            [1787] =>  * @param string $name The controller name.

            [1788] =>  *

            [1789] =>  * @return object The instantiated controller object.

            [1790] =>  */

            [1791] => function controller(string $name): object {

            [1792] => 	$controllerClass = 'Vvveb\Controller\\' . ucwords($name);

            [1793] => 

            [1794] => 	return new $controllerClass();

            [1795] => }

            [1796] => 

            [1797] => /**

            [1798] =>  * Return the fully qualified class name for a field type.

            [1799] =>  *

            [1800] =>  * @param string $fieldType The field type identifier.

            [1801] =>  *

            [1802] =>  * @return string|null The fully qualified class name, or null if not found.

            [1803] =>  */

            [1804] => function fieldTypeClass(string $fieldType): ?string {

            [1805] => 	$field      = ucfirst($fieldType);

            [1806] => 	$fieldClass = "Vvveb\System\Fields\\$field";

            [1807] => 

            [1808] => 	if (! class_exists($fieldClass)) {

            [1809] => 		return null;

            [1810] => 	}

            [1811] => 

            [1812] => 	return $fieldClass;

            [1813] => }

            [1814] => 

            [1815] => /**

            [1816] =>  * Returns available post types for a given type category.

            [1817] =>  *

            [1818] =>  * @param  string $type The type category (e.g. 'post', 'product').

            [1819] =>  * @return array

            [1820] =>  */

            [1821] => function postTypes(string $type): array {

            [1822] => 	$defaultTypes = [

            [1823] => 		'post' => [

            [1824] => 			'post' => [

            [1825] => 				'name'    => 'Post',

            [1826] => 				'source'  => 'default',

            [1827] => 				'site_id' => '0',

            [1828] => 				'type'    => 'post',

            [1829] => 				'plural'  => 'posts',

            [1830] => 				'icon'    => 'icon-document-text-outline',

            [1831] => 				'comments'=> true,

            [1832] => 			],

            [1833] => 			'page' => [

            [1834] => 				'name'    => 'Page',

            [1835] => 				'source'  => 'default',

            [1836] => 				'site_id' => '0',

            [1837] => 				'type'    => 'page',

            [1838] => 				'plural'  => 'pages',

            [1839] => 				'icon'    => 'icon-document-outline',

            [1840] => 				'comments'=> false,

            [1841] => 			],

            [1842] => 		],

            [1843] => 		'product' => [

            [1844] => 			'product' => [

            [1845] => 				'name'    => 'Product',

            [1846] => 				'type'    => 'product',

            [1847] => 				'site_id' => '0',

            [1848] => 				'source'  => 'default',

            [1849] => 				'plural'  => 'products',

            [1850] => 				'icon'    => 'icon-cube-outline',

            [1851] => 			],

            [1852] => 		],

            [1853] => 	];

            [1854] => 

            [1855] => 	$default               = $defaultTypes[$type] ?? [];

            [1856] => 	$typeName              = ucfirst($type);

            [1857] => 	list($pluginTypes)     = System\Event::trigger('Vvveb\postTypes', "custom$typeName", []);

            [1858] => 	array_walk($pluginTypes, function (&$type,$key) {

            [1859] => 		$type['source'] = 'plugin';

            [1860] => 		$type['name'] = ucfirst($key);

            [1861] => 	});

            [1862] => 

            [1863] => 	$userTypes = getSetting($type, 'types', []);

            [1864] => 

            [1865] => 	$params            = ['module' => "settings/{$type}-type"];

            [1866] => 	$paramsList        = ['module' => "settings/{$type}-types"];

            [1867] => 

            [1868] => 	array_walk($userTypes, function (&$type,$key) use ($params, $paramsList) {

            [1869] => 		$type['source'] = 'user';

            [1870] => 		$type['name'] = ucfirst($key);

            [1871] => 		$type['url']        = \Vvveb\url($params + ['type' => $type['type']]);

            [1872] => 		$type['delete-url'] = \Vvveb\url($paramsList + ['action' => 'delete', 'type[]' => $type['type']]);

            [1873] => 	});

            [1874] => 

            [1875] => 	$types = $default + $pluginTypes + $userTypes;

            [1876] => 

            [1877] => 	return $types;

            [1878] => }

            [1879] => 

            [1880] => /**

            [1881] =>  * Debug output function - dumps variables with syntax highlighting.

            [1882] =>  *

            [1883] =>  * @param  mixed ...$variables The variables to dump.

            [1884] =>  * @return void

            [1885] =>  */

            [1886] => function d(mixed ...$variables): void {

            [1887] => 	foreach ($variables as $variable) {

            [1888] => 		echo highlight_string(" 	}

            [1890] => }

            [1891] => 

            [1892] => /**

            [1893] =>  * Debug and die - dumps variables with syntax highlighting and stops execution.

            [1894] =>  *

            [1895] =>  * @param  mixed ...$variables The variables to dump.

            [1896] =>  * @return never

            [1897] =>  */

            [1898] => function dd(mixed ...$variables): never {

            [1899] => 	foreach ($variables as $variable) {

            [1900] => 		echo highlight_string(" 	}

            [1902] => 

            [1903] => 	die();

            [1904] => }

            [1905] => 

            [1906] => /**

            [1907] =>  * Encrypts a string using OpenSSL with specified cipher and digest.

            [1908] =>  *

            [1909] =>  * @param  string      $key        The encryption key.

            [1910] =>  * @param  string      $value      The plaintext string to encrypt.

            [1911] =>  * @param  string      $cipher     The cipher method (default 'aes256').

            [1912] =>  * @param  string      $digest     The digest method for key derivation (default 'sha256').

            [1913] =>  * @param  string|null &$tag       The authentication tag output.

            [1914] =>  * @param  string      $aad        Additional authenticated data.

            [1915] =>  * @param  int         $tag_length The authentication tag length (default 16).

            [1916] =>  * @return string      The base64-encoded encrypted string.

            [1917] =>  */

            [1918] => function encrypt(string $key, string $value, string $cipher = 'aes256', string $digest = 'sha256', ?string &$tag = null, string $aad = '', int $tag_length = 16): string {

            [1919] => 	$key       = openssl_digest($key, $digest, true);

            [1920] => 	$iv_length = openssl_cipher_iv_length($cipher);

            [1921] => 	$iv        = openssl_random_pseudo_bytes($iv_length);

            [1922] => 

            [1923] => 	return base64_encode($iv . openssl_encrypt($value, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag, $aad, $tag_length));

            [1924] => }

            [1925] => 

            [1926] => /**

            [1927] =>  * Decrypts an OpenSSL-encrypted string.

            [1928] =>  *

            [1929] =>  * @param  string       $key    The decryption key.

            [1930] =>  * @param  string       $value  The base64-encoded encrypted string.

            [1931] =>  * @param  string       $cipher The cipher method (default 'aes256').

            [1932] =>  * @param  string       $digest The digest method for key derivation (default 'sha256').

            [1933] =>  * @param  string|null  &$tag   The authentication tag output.

            [1934] =>  * @param  string       $aad    Additional authenticated data.

            [1935] =>  * @return string|false The decrypted string, or false on failure.

            [1936] =>  */

            [1937] => function decrypt(string $key, string $value, string $cipher = 'aes256', string $digest = 'sha256', ?string &$tag = null, string $aad = ''): string|false {

            [1938] => 	$result    = false;

            [1939] => 

            [1940] => 	$key       = openssl_digest($key, $digest, true);

            [1941] => 	$iv_length = openssl_cipher_iv_length($cipher);

            [1942] => 	$value     = base64_decode($value);

            [1943] => 	$iv        = substr($value, 0, $iv_length);

            [1944] => 	$value     = substr($value, $iv_length);

            [1945] => 

            [1946] => 	if (strlen($iv) == $iv_length) {

            [1947] => 		$result = openssl_decrypt($value, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag, $aad);

            [1948] => 	}

            [1949] => 

            [1950] => 	return $result;

            [1951] => }

            [1952] => 

            [1953] => /**

            [1954] =>  * Converts CamelCase string to dash/underscore-separated lowercase.

            [1955] =>  *

            [1956] =>  * @param  string $string The CamelCase string to convert.

            [1957] =>  * @param  string $us     The separator character (default '-').

            [1958] =>  * @return string The converted string.

            [1959] =>  */

            [1960] => function camelToUnderscore(string $string, string $us = '-'): string {

            [1961] => 	return strtolower(preg_replace(

            [1962] => 		'/(?<=\d)(?=[A-Za-z])|(?<=[A-Za-z])(?=\d)|(?<=[a-z])(?=[A-Z])/', $us, $string));

            [1963] => }

            [1964] => 

            [1965] => /**

            [1966] =>  * Recursively converts camelCase array keys to dash-separated lowercase.

            [1967] =>  *

            [1968] =>  * @param  array  &$array The array to convert keys for.

            [1969] =>  * @param  string $us     The separator character (default '-').

            [1970] =>  * @return array  The array with converted keys.

            [1971] =>  */

            [1972] => function arrayKeysToUnderscore(array &$array, string $us = '-'): array {

            [1973] => 	foreach ($array as $key => &$value) {

            [1974] => 		if (is_array($value)) {

            [1975] => 			$value = arrayKeysToUnderscore($value, $us);

            [1976] => 		}

            [1977] => 

            [1978] => 		$newKey = camelToUnderscore($key, $us);

            [1979] => 

            [1980] => 		if ($newKey != $key) {

            [1981] => 			$array[$newKey] = $value;

            [1982] => 			unset($array[$key]);

            [1983] => 		}

            [1984] => 	}

            [1985] => 

            [1986] => 	return $array;

            [1987] => }

            [1988] => 

            [1989] => /**

            [1990] =>  * Removes specified HTML tags from a string.

            [1991] =>  *

            [1992] =>  * @param  string $string The string to process.

            [1993] =>  * @param  array  $tags   The tag names to strip (default ['script', 'iframe', 'applet']).

            [1994] =>  * @return string The string with tags removed.

            [1995] =>  */

            [1996] => function stripTags(string $string, array $tags = ['script', 'iframe', 'applet']): string {

            [1997] => 	if ($string && $tags) {

            [1998] => 		foreach ($tags as $tag) {

            [1999] => 			$string = preg_replace('@<\s*' . $tag . '.*?>.*?@im', '', $string);

            [2000] => 		}

            [2001] => 	}

            [2002] => 

            [2003] => 	return $string;

            [2004] => }

            [2005] => 

            [2006] => /**

            [2007] =>  * Recursively strips tags from all string values in an array.

            [2008] =>  *

            [2009] =>  * @param  array $array The array to process.

            [2010] =>  * @param  array $tags  The tag names to strip.

            [2011] =>  * @return array The array with tags stripped from string values.

            [2012] =>  */

            [2013] => function stripTagsArray(array $array, array $tags): array {

            [2014] => 	if ($tags) {

            [2015] => 		foreach ($array as $key => &$value) {

            [2016] => 			if (is_string($value)) {

            [2017] => 				$value = stripTags($value, $tags);

            [2018] => 			} elseif (is_array($value)) {

            [2019] => 				$value = stripTagsArray($value, $tags);

            [2020] => 			}

            [2021] => 		}

            [2022] => 		unset($value);

            [2023] => 	}

            [2024] => 

            [2025] => 	return $array;

            [2026] => }

            [2027] => 

            [2028] => /**

            [2029] =>  * Removes specified characters from a string.

            [2030] =>  *

            [2031] =>  * @param  string $string The string to process.

            [2032] =>  * @param  array  $chars  The characters to remove.

            [2033] =>  * @return string The string with characters removed.

            [2034] =>  */

            [2035] => function stripChars(string $string, array $chars = ['(', ')', '{', '}', ':', '<', '>']): string {

            [2036] => 	return str_replace($chars, '', $string);

            [2037] => }

            [2038] => 

            [2039] => /**

            [2040] =>  * Sanitizes HTML by removing dangerous elements and attributes.

            [2041] =>  *

            [2042] =>  * @param  mixed $string The string to sanitize. Non-string values are returned as-is.

            [2043] =>  * @return mixed The sanitized string, or the original value if not a string.

            [2044] =>  */

            [2045] => function sanitizeHTML(mixed $string): mixed {

            [2046] => 	if (! is_string($string)) {

            [2047] => 		return $string;

            [2048] => 	}

            [2049] => 

            [2050] => 	// Fix &entity\n;

            [2051] => 	//add <> before tag entities to allow regex matching

            [2052] => 	$string = str_replace(['<', '>'], ['<<', '>>'], $string);

            [2053] => 	$string = str_replace(['&', '<', '>'], ['&amp;', '&lt;', '&gt;'], $string);

            [2054] => 	$string = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '$1;', $string);

            [2055] => 	$string = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $string);

            [2056] => 	$string = html_entity_decode($string, ENT_COMPAT, 'UTF-8');

            [2057] => 

            [2058] => 	// Remove any attribute starting with "on" or xmlns

            [2059] => 	$string = preg_replace('#(<[^>]+?[\x00-\x20"\'/])(?:on|xmlns)[^>]*+>#iu', '$1>', $string);

            [2060] => 	$string = preg_replace('#[\x00-\x20"\'/](on|xmlns)[:\w]+[\x00-\x20"\'/]*=[\x00-\x20"\'/]*([\'"]+).+?\2#iu', '', $string);

            [2061] => 

            [2062] => 	// Remove javascript: and vbscript: protocols

            [2063] => 	$string = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $string);

            [2064] => 	$string = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $string);

            [2065] => 	$string = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding...', $string);

            [2066] => 

            [2067] => 	// Only works in IE: 

            [2068] => 	$string = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#i', '$1>', $string);

            [2069] => 	$string = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#i', '$1>', $string);

            [2070] => 	$string = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#iu', '$1>', $string);

            [2071] => 

            [2072] => 	// Remove namespaced elements (we do not need them)

            [2073] => 	$string = preg_replace('#]*+>#i', '', $string);

            [2074] => 

            [2075] => 	do {

            [2076] => 		// Remove really unwanted tags

            [2077] => 		$old_data = $string;

            [2078] => 		//$string   = preg_replace('#]*+>#i', '', $string);

            [2079] => 		$string   = preg_replace('#]*+>#i', '', $string);

            [2080] => 	} while ($old_data !== $string);

            [2081] => 

            [2082] => 	//restore entities

            [2083] => 	$string = str_replace(['<<', '>>'], ['<', '>'], $string);

            [2084] => 	return $string;

            [2085] => }

            [2086] => 

            [2087] => 

            [2088] => /**

            [2089] =>  * Returns site settings with caching and optional language filtering.

            [2090] =>  *

            [2091] =>  * @param  int       $site_id     The site ID (default SITE_ID).

            [2092] =>  * @param  int|false $language_id The language ID to filter by, or false to skip filtering.

            [2093] =>  * @return array     The site settings array.

            [2094] =>  */

            [2095] => function siteSettings(int $site_id = SITE_ID, int|false $language_id = false): array {

            [2096] => 	$cache     = System\Cache::getInstance();

            [2097] => 	$site      = $cache->cache(APP,'site.' . $site_id, function () use ($site_id) {

            [2098] => 		$siteSql             = new Sql\SiteSQL();

            [2099] => 		$site                = $siteSql->get(['site_id' => $site_id]);

            [2100] => 

            [2101] => 		if ($site && isset($site['settings'])) {

            [2102] => 			$settings = json_decode($site['settings'], true);

            [2103] => 			$siteData = $siteSql->getSiteData(['site' => $settings]);

            [2104] => 

            [2105] => 			foreach (['favicon', 'logo', 'logo-sticky', 'logo-dark', 'logo-dark-sticky', 'webbanner'] as $img) {

            [2106] => 				if (isset($settings[$img])) {

            [2107] => 					$settings["$img-src"] = $settings[$img];

            [2108] => 					$settings[$img] = System\Images::image($settings[$img], '');

            [2109] => 				}

            [2110] => 			}

            [2111] => 

            [2112] => 			return $settings + $siteData;

            [2113] => 		}

            [2114] => 

            [2115] => 		return [];

            [2116] => 	}, 259200);

            [2117] => 

            [2118] => 	if ($language_id && $site) {

            [2119] => 		$site['description'] = $site['description'][$language_id] ?? $site['description'][1] ?? '';

            [2120] => 	}

            [2121] => 

            [2122] => 	return $site;

            [2123] => }

            [2124] => 

            [2125] => /**

            [2126] =>  * Check php code for syntax errors, return false on errors.

            [2127] =>  * 

            [2128] =>  * @param  string $source

            [2129] =>  * @return bool

            [2130] =>  */

            [2131] => function checkPhpSyntax(string $source): bool {

            [2132] => 	try {

            [2133] => 		$tokens = token_get_all($source, TOKEN_PARSE);

            [2134] => 	} catch (\ParseError $e) {

            [2135] => 		return false;

            [2136] => 	}

            [2137] => 

            [2138] => 	return $tokens !== false;

            [2139] => }

            [2140] => 

            [2141] => /**

            [2142] =>  * Truncates text to specified number of characters.

            [2143] =>  *

            [2144] =>  * @param  string $text  The text to truncate.

            [2145] =>  * @param  int    $limit The maximum number of characters.

            [2146] =>  * @return string The truncated text.

            [2147] =>  */

            [2148] => function truncateWords(string $text, int $limit): string {

            [2149] => 	$parts = preg_split('/(\s+)/ms', substr($text, 0, $limit * 2), 0, PREG_SPLIT_DELIM_CAPTURE);

            [2150] => 	$partsCount = count($parts);

            [2151] => 

            [2152] => 	$length = 0;

            [2153] => 	$lastPart = 0;

            [2154] => 	for (; $lastPart < $partsCount; ++$lastPart) {

            [2155] => 		$partlen = strlen($parts[$lastPart]);

            [2156] => 		$length += $partlen;

            [2157] => 		if ($length >= $limit) {

            [2158] => 			//don't include less than two letter last word or spaces

            [2159] => 			$partlen = strlen($parts[$lastPart - 1]);

            [2160] => 			while (($partlen < 3) && $lastPart > 3) {

            [2161] => 				$lastPart--;

            [2162] => 				$partlen = strlen($parts[$lastPart - 1]);

            [2163] => 			}

            [2164] => 			break;

            [2165] => 		}

            [2166] => 	}

            [2167] => 

            [2168] => 	return implode(array_slice($parts, 0, $lastPart));

            [2169] => }

            [2170] => 

            [2171] => /**

            [2172] =>  * Send email using template.

            [2173] =>  * 

            [2174] =>  * @param  string|array $to

            [2175] =>  * @param  mixed        $template

            [2176] =>  * @param  string       $subject

            [2177] =>  * @param  array        $data

            [2178] =>  * @param  mixed        $config

            [2179] =>  * @param  null|mixed   $app

            [2180] =>  * @param  mixed        $site_id

            [2181] =>  * @return bool

            [2182] =>  */

            [2183] => function email(string|array $to, string $subject, string|array $template, array $data = [], array $config = [], ?string $app = null, int $site_id = SITE_ID): bool {

            [2184] => 	$email = System\Email::getInstance();

            [2185] => 

            [2186] => 	if (is_array($template)) {

            [2187] => 		$html = $template['html'] ?? '';

            [2188] => 		$txt  = $template['txt'] ?? '';

            [2189] => 	} else {

            [2190] => 		$htmlView  = new System\Core\View($app);

            [2191] => 		$htmlView->setTheme();

            [2192] => 		$htmlView->set($data);

            [2193] => 		//get email html template

            [2194] => 		$htmlView->template("email/$template.html");

            [2195] => 		$html = $htmlView->render(true, false, true);

            [2196] => 

            [2197] => 		//get email text template

            [2198] => 		$txtView  = new System\Core\View($app);

            [2199] => 		$txtView->setTheme();

            [2200] => 		$txtView->set($data);

            [2201] => 		$txtView->template("email/$template.txt.html");

            [2202] => 		$txt = $txtView->render(true, false, true);

            [2203] => 		$txt = htmlToText($txt);

            [2204] => 		/*

            [2205] => 		$htmlView->template("email/$template.txt.html");

            [2206] => 		$txt = $htmlView->render(true, false, true);

            [2207] => 		 */

            [2208] => 	}

            [2209] => 

            [2210] => 	//get site contact email for sender and reply to

            [2211] => 	$site   = siteSettings($site_id, session('language_id') ?? 1);

            [2212] => 	$sender = $config['sender'] ?? $site['description']['title'];

            [2213] => 	$from   = $config['from'] ?? $site['contact-email'];

            [2214] => 	$reply  = $config['reply'] ?? $site['contact-email'];

            [2215] => 

            [2216] => 	$email->setHtml($html);

            [2217] => 	$email->setText($txt);

            [2218] => 	$email->setTo($to);

            [2219] => 	$email->setSender($sender);

            [2220] => 	$email->setFrom($from);

            [2221] => 	$email->setReplyTo($reply);

            [2222] => 	$email->setSubject($subject);

            [2223] => 

            [2224] => 	return $email->send();

            [2225] => }

            [2226] => 

            [2227] => /**

            [2228] =>  * Recursively removes a directory.

            [2229] =>  *

            [2230] =>  * @param  string $src        The directory path to remove.

            [2231] =>  * @param  array  $skip       Files or directories to skip (currently unused).

            [2232] =>  * @param  mixed  $keepParent

            [2233] =>  * @return bool   True on success, false on failure.

            [2234] =>  */

            [2235] => function rrmdir(string $src, array $skip = [], $keepParent = false): bool {

            [2236] => 	if (! is_dir($src)) {

            [2237] => 		return false;

            [2238] => 	}

            [2239] => 

            [2240] => 	$dir = @opendir($src);

            [2241] => 	$result = true;

            [2242] => 

            [2243] => 	if ($dir) {

            [2244] => 		while (false !== ($file = readdir($dir))) {

            [2245] => 			$full = $src . DS . $file;

            [2246] => 

            [2247] => 			if (($file != '.') &&

            [2248] => 				($file != '..') &&

            [2249] => 				(! in_array($full, $skip))) {

            [2250] => 				if (is_dir($full)) {

            [2251] => 					$result = rrmdir($full, $skip, false);

            [2252] => 					if ($result) {

            [2253] => 						$result = rmdir($full);

            [2254] => 					}

            [2255] => 				} else {

            [2256] => 					$result = @unlink($full);

            [2257] => 				}

            [2258] => 

            [2259] => 				if (! $result) {

            [2260] => 					break;

            [2261] => 				}

            [2262] => 			}

            [2263] => 		}

            [2264] => 

            [2265] => 		closedir($dir);

            [2266] => 

            [2267] => 		if ($result && ! $keepParent) {

            [2268] => 			//rmdir($full);

            [2269] => 		}

            [2270] => 	}

            [2271] => 

            [2272] => 	return $result;

            [2273] => }

            [2274] => 

            [2275] => /**

            [2276] =>  * Recursively copies a directory.

            [2277] =>  *

            [2278] =>  * @param  string $src       The source directory path.

            [2279] =>  * @param  string $dst       The destination directory path.

            [2280] =>  * @param  array  $skip      Files or directories to skip.

            [2281] =>  * @param  bool   $overwrite Whether to overwrite existing destination (default true).

            [2282] =>  * @return bool   True on success, false on failure.

            [2283] =>  */

            [2284] => function rcopy(string $src, string $dst, array $skip = [], bool $overwrite = true): bool {

            [2285] => 	if ($overwrite && file_exists($dst)) {

            [2286] => 		//rrmdir($dst);

            [2287] => 	}

            [2288] => 

            [2289] => 	$result = true;

            [2290] => 

            [2291] => 	if (is_dir($src)) {

            [2292] => 		if (! file_exists($dst)) {

            [2293] => 			$result = @mkdir($dst);

            [2294] => 

            [2295] => 			if (! $result) {

            [2296] => 				return false;

            [2297] => 			}

            [2298] => 		}

            [2299] => 

            [2300] => 		@chmod($dst, 0755);

            [2301] => 

            [2302] => 		$files = scandir($src);

            [2303] => 

            [2304] => 		foreach ($files as $file) {

            [2305] => 			$full = $src . DS . $file;

            [2306] => 

            [2307] => 			if ($file != '.' &&

            [2308] => 				$file != '..' &&

            [2309] => 				! in_array($file, $skip)) {

            [2310] => 				if (is_dir($full)) {

            [2311] => 					$result = rcopy($full, $dst . DS . $file);

            [2312] => 				} else {

            [2313] => 					$result = @copy($full, $dst . DS . $file);

            [2314] => 

            [2315] => 					if (! $result) {

            [2316] => 						if (@chmod($dst . DS . $file, 0644)) {

            [2317] => 							$result = @copy($full, $dst . DS . $file);

            [2318] => 						}

            [2319] => 					}

            [2320] => 				}

            [2321] => 

            [2322] => 				if (! $result) {

            [2323] => 					return false;

            [2324] => 				}

            [2325] => 			}

            [2326] => 		}

            [2327] => 	} else {

            [2328] => 		if (file_exists($src)) {

            [2329] => 			$result = @copy($src, $dst);

            [2330] => 

            [2331] => 			if (! $result) {

            [2332] => 				if (@chmod($dst, 0644)) {

            [2333] => 					$result = copy($src, $dst);

            [2334] => 				}

            [2335] => 			}

            [2336] => 

            [2337] => 			if (! $result) {

            [2338] => 				return false;

            [2339] => 			}

            [2340] => 		}

            [2341] => 	}

            [2342] => 

            [2343] => 	return $result;

            [2344] => }

            [2345] => 

            [2346] => /**

            [2347] =>  * Recursively renames/moves a directory.

            [2348] =>  *

            [2349] =>  * @param  string $src  The source directory path.

            [2350] =>  * @param  string $dst  The destination directory path.

            [2351] =>  * @param  array  $skip Files or directories to skip.

            [2352] =>  * @return void

            [2353] =>  */

            [2354] => function rrename(string $src, string $dst, array $skip = []): void {

            [2355] => 	if (file_exists($dst)) {

            [2356] => 		rrmdir($dst);

            [2357] => 	}

            [2358] => 

            [2359] => 	if (is_dir($src)) {

            [2360] => 		if (! is_dir($dst)) {

            [2361] => 			mkdir($dst, 0755, true);

            [2362] => 		}

            [2363] => 		$files = scandir($src);

            [2364] => 

            [2365] => 		foreach ($files as $file) {

            [2366] => 			if ($file != '.' &&

            [2367] => 				$file != '..' &&

            [2368] => 				! in_array($file, $skip)) {

            [2369] => 				//rrename("$src/$file", "$dst/$file");

            [2370] => 				rename("$src/$file", "$dst/$file");

            [2371] => 			}

            [2372] => 		}

            [2373] => 	} else {

            [2374] => 		if (file_exists($src)) {

            [2375] => 			rename($src, $dst);

            [2376] => 		}

            [2377] => 	}

            [2378] => }

            [2379] => 

            [2380] => /**

            [2381] =>  * Downloads content from a URL using cURL or file_get_contents.

            [2382] =>  *

            [2383] =>  * @param  string      $url The URL to download.

            [2384] =>  * @return string|null The downloaded content, or null on failure.

            [2385] =>  */

            [2386] => function download(string $url): ?string {

            [2387] => 	$result = false;

            [2388] => 

            [2389] => 	$url = html_entity_decode($url);

            [2390] => 

            [2391] => 	if (function_exists('curl_init')) {

            [2392] => 		$ch = curl_init($url);

            [2393] => 

            [2394] => 		if ($ch) {

            [2395] => 			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

            [2396] => 			//curl_setopt($ch, CURLOPT_RESOLVE, $ips);

            [2397] => 			curl_setopt($ch, CURLOPT_FAILONERROR, true);

            [2398] => 			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);

            [2399] => 			curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT'] ?? 'Vvveb ' . V_VERSION);

            [2400] => 			$result   = curl_exec($ch);

            [2401] => 			$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

            [2402] => 

            [2403] => 			if (PHP_MAJOR_VERSION < 8) {

            [2404] => 				curl_close($ch);

            [2405] => 			}

            [2406] => 

            [2407] => 			if ($httpcode != 200) {

            [2408] => 				return null;

            [2409] => 			}

            [2410] => 		}

            [2411] => 	} else {

            [2412] => 		if (ini_get('allow_url_fopen') == '1') {

            [2413] => 			$context_options = [

            [2414] => 				'http' => [

            [2415] => 					'timeout'       => 5,

            [2416] => 					'ignore_errors' => 1,

            [2417] => 				],

            [2418] => 			];

            [2419] => 			$context  = stream_context_create($context_options);

            [2420] => 			$result   = file_get_contents($url, 'r', $context);

            [2421] => 		}

            [2422] => 	}

            [2423] => 

            [2424] => 	return $result;

            [2425] => }

            [2426] => 

            [2427] => /**

            [2428] =>  * Validates URL and resolves hostname to prevent SSRF attacks.

            [2429] =>  *

            [2430] =>  * @param  string       $url The URL to validate.

            [2431] =>  * @return string|array The cURL resolve entries, an empty string for invalid URLs, or an empty array for private/reserved IPs.

            [2432] =>  */

            [2433] => function validateUrl(string $url): array {

            [2434] => 	$p = parse_url($url);

            [2435] => 	$scheme = $p['scheme'] ?? 'https';

            [2436] => 	$port   = $p['port'] ?? ($scheme === 'http' ? '80' : '443');

            [2437] => 

            [2438] => 	if (! $p || ! in_array($scheme, ['http', 'https'], true) || ! in_array($port, ['80', '443'], true) || empty($p['host'])) {

            [2439] => 		return [];

            [2440] => 	}

            [2441] => 

            [2442] => 	if (($scheme === 'http' && $port !== '80') || ($scheme === 'https' && $port !== '443')) {

            [2443] => 		return [];

            [2444] => 	}

            [2445] => 

            [2446] => 	$ips = gethostbynamel($p['host']) ?: [];

            [2447] => 

            [2448] => 	$results = [];

            [2449] => 	foreach ($ips as $ip) {

            [2450] => 		$results[] = $p['host'] . ':' . $port . ':[' . $ip . ']';

            [2451] => 

            [2452] => 		if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {

            [2453] => 			return []; // private / reserved / loopback / link-local

            [2454] => 		}

            [2455] => 	}

            [2456] => 

            [2457] => 	return $results;

            [2458] => }

            [2459] => 

            [2460] => /**

            [2461] =>  * Fetches URL content with caching support.

            [2462] =>  *

            [2463] =>  * @param  string $url       The URL to fetch.

            [2464] =>  * @param  bool   $cache     Whether to cache the result (default true).

            [2465] =>  * @param  int    $expire    Cache expiration in seconds (default 604800).

            [2466] =>  * @param  int    $timeout   Request timeout in seconds (default 5).

            [2467] =>  * @param  bool   $exception Whether to throw exceptions on failure (default true).

            [2468] =>  * @return mixed  The fetched content, or null on failure.

            [2469] =>  */

            [2470] => function getUrl(string $url, bool $cache = true, int $expire = 604800, int $timeout = 5, bool $exception = true): mixed {

            [2471] => 	$cacheDriver  = System\Cache :: getInstance();

            [2472] => 	$cacheKey     = md5($url);

            [2473] => 	$result       = false;

            [2474] => 

            [2475] => 

            [2476] => 	if ($cache && ($result = $cacheDriver->get('url', $cacheKey))) {

            [2477] => 		return $result;

            [2478] => 	} else {

            [2479] => 		$result = false;

            [2480] => 		$url    = html_entity_decode($url);

            [2481] => 		//try with curl

            [2482] => 		if (function_exists('curl_init')) {

            [2483] => 			$ch = curl_init($url);

            [2484] => 

            [2485] => 			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

            [2486] => 			//curl_setopt($ch, CURLOPT_RESOLVE, $ips);

            [2487] => 			curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);

            [2488] => 			curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);

            [2489] => 			curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT'] ?? 'Vvveb ' . V_VERSION);

            [2490] => 

            [2491] => 			if (DEBUG) {

            [2492] => 				curl_setopt($ch, CURLOPT_VERBOSE, true);

            [2493] => 				$streamVerboseHandle = fopen('php://temp', 'w+');

            [2494] => 				curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);

            [2495] => 			}

            [2496] => 

            [2497] => 			$result = curl_exec($ch);

            [2498] => 

            [2499] => 			if ($result) {

            [2500] => 				$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

            [2501] => 				if ($httpcode != 200) {

            [2502] => 					return null;

            [2503] => 				}

            [2504] => 

            [2505] => 				if ($cache) {

            [2506] => 					$cacheDriver->set('url', $cacheKey, $result);

            [2507] => 				}

            [2508] => 

            [2509] => 				return $result;

            [2510] => 			} else {

            [2511] => 				if ($exception) {

            [2512] => 					$message = 'Curl error: ' . curl_errno($ch) . ' - ' . curl_error($ch);

            [2513] => 

            [2514] => 					if (DEBUG) {

            [2515] => 						$message .= "\n" . print_r(curl_getinfo($ch), 1);

            [2516] => 						rewind($streamVerboseHandle);

            [2517] => 						$message .= "\n" . stream_get_contents($streamVerboseHandle);

            [2518] => 					}

            [2519] => 

            [2520] => 					throw new \Exception($message);

            [2521] => 				}

            [2522] => 			}

            [2523] => 

            [2524] => 			if (PHP_MAJOR_VERSION < 8) {

            [2525] => 				curl_close($ch);

            [2526] => 			}

            [2527] => 		} else {

            [2528] => 			//try with file get contents

            [2529] => 			if (ini_get('allow_url_fopen') == '1') {

            [2530] => 				$context_options = [

            [2531] => 					'http' => [

            [2532] => 						'timeout'       => $timeout,

            [2533] => 						'ignore_errors' => 1,

            [2534] => 					],

            [2535] => 				];

            [2536] => 

            [2537] => 				$context         = stream_context_create($context_options);

            [2538] => 

            [2539] => 				if ($exception) {

            [2540] => 					$result = file_get_contents($url, false, $context);

            [2541] => 				} else {

            [2542] => 					$result = @file_get_contents($url, false, $context);

            [2543] => 				}

            [2544] => 			}

            [2545] => 

            [2546] => 			if ($result) {

            [2547] => 				if ($cache) {

            [2548] => 					$cacheDriver->set('url', $cacheKey, $result);

            [2549] => 				}

            [2550] => 

            [2551] => 				return $result;

            [2552] => 			}

            [2553] => 		}

            [2554] => 	}

            [2555] => 

            [2556] => 	return $result;

            [2557] => }

            [2558] => 

            [2559] => /**

            [2560] =>  * Extracts a zip archive.

            [2561] =>  *

            [2562] =>  * @param  string $file The path to the zip file to extract.

            [2563] =>  * @return bool   True on success, false on failure.

            [2564] =>  */

            [2565] => function unzip(string $file): bool {

            [2566] => 	// get the absolute path to $file

            [2567] => 	$path = pathinfo(realpath($file), PATHINFO_DIRNAME);

            [2568] => 

            [2569] => 	$zip = new \ZipArchive();

            [2570] => 	$res = $zip->open($file);

            [2571] => 

            [2572] => 	if ($res === true) {

            [2573] => 		$zip->extractTo($path);

            [2574] => 		$zip->close();

            [2575] => 

            [2576] => 		return true;

            [2577] => 	}

            [2578] => 

            [2579] => 	return false;

            [2580] => }

            [2581] => 

            [2582] => /**

            [2583] =>  * Converts an HTML string to plain text.

            [2584] =>  *

            [2585] =>  * @param  string $html The HTML string to convert.

            [2586] =>  * @return string The plain text result.

            [2587] =>  */

            [2588] => function htmlToText(string $html): string {

            [2589] => 	$html = preg_replace('/\s+/', ' ', $html);

            [2590] => 	$html = str_replace('
', "\n", $html); [2591] => //add new line for block elements [2592] => $html = preg_replace('/<(div|p|li|hr|pre|blockquote|section|h\d+)/', "\n<$1", $html); [2593] => [2594] => return trim(strip_tags($html)); [2595] => } [2596] => [2597] => /** [2598] => * Wraps double-newline-separated text in paragraph tags. [2599] => * [2600] => * @param string $string The text to wrap. [2601] => * @return string The text wrapped in

tags. [2602] => */ [2603] => function nl2p(string $string): string { [2604] => $paragraphs = ''; [2605] => [2606] => foreach (explode("\n\n", $string) as $line) { [2607] => if (trim($line)) { [2608] => $paragraphs .= "

$line

"; [2609] => } [2610] => } [2611] => [2612] => return $paragraphs; [2613] => } [2614] => [2615] => /** [2616] => * Returns Bootstrap CSS class for order status badge. [2617] => * [2618] => * @param int $order_status_id Order status ID (1-7) [2619] => * @return string CSS class string for the badge [2620] => */ [2621] => function orderStatusBadgeClass(int $order_status_id = 1): string { [2622] => $classes = [ [2623] => 1 => 'bg-primary-subtle text-body', //pending [2624] => 2 => 'bg-info-subtle text-body', //processing [2625] => 3 => 'bg-primary', //processed [2626] => 4 => 'bg-success', //complete [2627] => 5 => 'bg-danger', //canceled [2628] => 6 => 'bg-secondary', //archived [2629] => 7 => 'bg-warning', //requires_action [2630] => ]; [2631] => [2632] => return $classes[$order_status_id] ?? 'bg-warning text-dark'; [2633] => } [2634] => [2635] => /** [2636] => * Returns Bootstrap CSS class for payment status badge. [2637] => * [2638] => * @param int $payment_status_id Payment status ID (1-10) [2639] => * @return string CSS class string for the badge [2640] => */ [2641] => function paymentStatusBadgeClass(int $payment_status_id = 1): string { [2642] => $classes = [ [2643] => 1 => 'bg-warning-subtle text-body', //not_paid [2644] => 2 => 'bg-success-subtle text-body', //awaiting [2645] => 3 => 'bg-danger-subtle text-body', //captured [2646] => 4 => 'bg-success', //paid [2647] => 5 => 'bg-danger', //canceled [2648] => 6 => 'bg-danger', //refunded [2649] => 7 => 'bg-danger', //partially_refunded [2650] => 8 => 'bg-danger', //chargeback [2651] => 9 => 'bg-danger', //requires_action [2652] => 10 => 'bg-danger', //fraud [2653] => ]; [2654] => [2655] => return $classes[$payment_status_id] ?? 'bg-warning text-dark'; [2656] => } [2657] => [2658] => /** [2659] => * Returns Bootstrap CSS class for shipping status badge. [2660] => * [2661] => * @param int $shipping_status_id Shipping status ID (1-10) [2662] => * @return string CSS class string for the badge [2663] => */ [2664] => function shippingStatusBadgeClass(int $shipping_status_id = 1): string { [2665] => $classes = [ [2666] => 1 => 'bg-warning-subtle text-body', //not_fulfilled [2667] => 2 => 'bg-success-subtle text-body', //fulfilled [2668] => 3 => 'bg-success-subtle text-body', //partially_fulfilled [2669] => 4 => 'bg-success', //shipped [2670] => 5 => 'bg-success-subtle text-body', //partially_shipped [2671] => 6 => 'bg-danger-subtle text-body', //returned [2672] => 7 => 'bg-danger-subtle text-body', //partially_returned [2673] => 8 => 'bg-success', //delivered [2674] => 9 => 'bg-danger', //canceled [2675] => 10 => 'bg-warning', //requires_action [2676] => ]; [2677] => [2678] => return $classes[$shipping_status_id] ?? 'bg-warning text-dark'; [2679] => } [2680] => [2681] => /** [2682] => * Returns Bootstrap CSS class for comment status badge. [2683] => * [2684] => * @param int $status Comment status ID (0-3) [2685] => * @return string CSS class string for the badge [2686] => */ [2687] => function commentStatusBadgeClass(int $status = 0): string { [2688] => $classes = [ [2689] => 0 => 'bg-primary-subtle text-body', //pending [2690] => 1 => 'bg-success-subtle text-body', //approved [2691] => 2 => 'bg-warning-subtle text-body', //spam [2692] => 3 => 'bg-danger-subtle text-body', //trash [2693] => ]; [2694] => [2695] => return $classes[$status] ?? 'bg-secondary-subtle text-dark'; [2696] => } [2697] => [2698] => /** [2699] => * Glob with brace expansion support (fallback for systems without GLOB_BRACE). [2700] => * [2701] => * @param string $path Directory path [2702] => * @param array $glob Array of glob patterns [2703] => * @param string $filename Optional filename suffix [2704] => * @return array Array of matching file paths [2705] => */ [2706] => function globBrace(string $path, array $glob, string $filename = ''): array { [2707] => //$glob = ['*','*/*','*/*/*']; [2708] => $files = []; [2709] => [2710] => if (defined('GLOB_BRACE')) { [2711] => $path .= '{' . implode(',', $glob) . '}' . $filename; [2712] => $files = glob($path, GLOB_BRACE); [2713] => } else { [2714] => foreach ($glob as $pattern) { [2715] => $files = array_merge($files, glob($path . DS . $pattern . $filename)); [2716] => } [2717] => } [2718] => [2719] => return $files; [2720] => } [2721] => [2722] => /** [2723] => * Checks if the current request is over HTTPS. [2724] => * [2725] => * @return bool True if request is secure, false otherwise [2726] => */ [2727] => function isSecure(): bool { [2728] => return [2729] => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') [2730] => || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') [2731] => || ($_SERVER['SERVER_PORT'] ?? 80) == 443; [2732] => } [2733] => [2734] => /** [2735] => * Generates a string of random digits of specified length. [2736] => * [2737] => * @param int $length Number of digits to generate [2738] => * @return string Random digit string [2739] => */ [2740] => function randomDigits(int $length): string { [2741] => $result = ''; [2742] => [2743] => for ($i = 0; $i < $length; $i++) { [2744] => $result .= random_int(0, 9); [2745] => } [2746] => [2747] => return $result; [2748] => } [2749] => [2750] => /** [2751] => * Formats invoice number by replacing template placeholders with data. [2752] => * [2753] => * @param string $format The format string containing {placeholders} like {year}, {month}, {rand-no-6} [2754] => * @param array $data Array of replacement values [2755] => * [2756] => * @return string The formatted invoice string [2757] => */ [2758] => function invoiceFormat(string $format, array $data): string { [2759] => if (! $format) { [2760] => return ''; [2761] => } [2762] => $data += ['date_added' => time(), 'year' => date('Y'), 'year2' => date('y'), 'month_name' => date('M'), 'month' => date('m'), 'day' => date('d')]; [2763] => [2764] => return preg_replace_callback('/{(.+?)}/', function ($matches) use ($data) { [2765] => $key = $matches[1]; [2766] => [2767] => if (isset($data[$key])) { [2768] => return $data[$key]; [2769] => } [2770] => [2771] => if (strpos($key, 'rand-no-') !== false) { [2772] => $length = (int) str_replace('rand-no-', '', $key); [2773] => [2774] => return randomDigits($length); [2775] => } [2776] => [2777] => if (strpos($key, 'rand-str-') !== false) { [2778] => $length = (int) str_replace('rand-str-', '', $key); [2779] => [2780] => return strtoupper(System\Functions\Str::random($length)); [2781] => } [2782] => [2783] => return $matches[0]; [2784] => }, $format); [2785] => } [2786] => [2787] => /** [2788] => * Removes XML version declaration to avoid issues with PHP short open tags. [2789] => * [2790] => * @param string|null $xmlString The XML string to process [2791] => * [2792] => * @return string The XML string without the version declaration [2793] => */ [2794] => function xmlStripVersionDeclr(?string $xmlString): string { [2795] => $xmlString = preg_replace('/<\?xml\s+version[^\?]+\?>\s*/', '', $xmlString ?? ''); [2796] => //xml stylesheet [2797] => $xmlString = str_replace('', $xmlString); [2798] => [2799] => return $xmlString; [2800] => } [2801] => [2802] => /** [2803] => * Converts a PHP array to XML string using SimpleXMLElement. [2804] => * [2805] => * Supports attribute encoding via @@ prefix and type preservation markers (--array, --boolean, --int, --null, --object). [2806] => * [2807] => * @param array $array The array to convert [2808] => * @param \SimpleXMLElement|false $xml Existing XML element to append to, or false to create new [2809] => * [2810] => * @return string The XML string representation [2811] => */ [2812] => function array2xml(array $array, \SimpleXMLElement|false $xml = false): string { [2813] => if ($xml === false) { [2814] => $xml = new \SimpleXMLElement(''); [2815] => } [2816] => [2817] => foreach ($array as $key => $value) { [2818] => $attributes = []; [2819] => $processAttr = function (&$key) use (&$attributes) { [2820] => $i = 0; [2821] => $start = 0; [2822] => [2823] => if ($key) { [2824] => while (($start = strpos($key, '@@', $start))) { [2825] => $i++; [2826] => $start += 2; [2827] => $end = (int)strpos($key,' ', $start); [2828] => $name = substr($key, $start, $end ? $end - $start : null); [2829] => [2830] => if ($end == 0) { [2831] => $start += strlen($name); [2832] => } else { [2833] => $start = (int)$end; [2834] => } [2835] => [2836] => if ($name) { [2837] => $name = explode('=', $name); [2838] => $attributes[$name[0]] = trim($name[1] ?? '', '\'"'); [2839] => } [2840] => } [2841] => [2842] => $key = strstr($key, ' @@', true) ?: $key; [2843] => } [2844] => }; [2845] => [2846] => $processAttr($key); [2847] => [2848] => if (is_array($value) || is_object($value)) { [2849] => $node = $xml->addChild($key); [2850] => [2851] => if ($attributes) { [2852] => foreach ($attributes as $key => $val) { [2853] => $node->addAttribute($key, htmlspecialchars($val)); [2854] => } [2855] => } [2856] => array2xml($value, $node); [2857] => } else { [2858] => if (substr_compare($key, '--xmlattr', -9, 9) === 0) { [2859] => $key = substr($key, 0, -9); [2860] => $xml->addAttribute($key, htmlspecialchars($value)); [2861] => } else { [2862] => $processAttr($value); [2863] => [2864] => if ($value && is_string($value)) { [2865] => $value = htmlspecialchars($value); [2866] => } [2867] => $node = $xml->addChild($key, $value); [2868] => [2869] => if ($attributes) { [2870] => foreach ($attributes as $key => $val) { [2871] => $node->addAttribute($key, $val); [2872] => } [2873] => } [2874] => } [2875] => } [2876] => } [2877] => [2878] => $xmlString = $xml->asXML(); [2879] => [2880] => return xmlStripVersionDeclr($xmlString); [2881] => } [2882] => [2883] => /** [2884] => * Removes single-line and multi-line comments from a JSON string. [2885] => * [2886] => * @param string $json The JSON string with comments [2887] => * [2888] => * @return string The JSON string without comments [2889] => */ [2890] => function removeJsonComments(string $json): string { [2891] => $json = preg_replace('@^\s*//.*([\r\n]|\s)*|^\s*/\*([\r\n]|.)*?\*/([\r\n]|\s)*@m', '', $json); [2892] => [2893] => return $json; [2894] => } [2895] => [2896] => /** [2897] => * Encodes invalid XML name characters to numeric entity format (_xNNN_). [2898] => * [2899] => * @param string $name The XML element or attribute name to encode [2900] => * [2901] => * @return string The encoded name with invalid characters replaced [2902] => */ [2903] => function encodeXmlName(string $name): string { [2904] => $len = strlen($name); [2905] => $newName = ''; [2906] => [2907] => for ($i = 0; $i < $len; $i++) { [2908] => $char = $name[$i]; [2909] => $code = ord($char); [2910] => [2911] => // = @ _ - . A-Z a-z 0-9 [2912] => if (($code >= 48 && $code <= 57) || ($code >= 65 && $code <= 90) || ($code >= 97 && $code <= 122) || $code === 32 || $code === 34 || $code === 45 || $code === 46 || $code === 61 || $code === 64 || $code === 95) { [2913] => $char = '_x' . sprintf('%03d', $code) . '_'; [2914] => } [2915] => [2916] => $newName .= $char; [2917] => } [2918] => [2919] => return $newName; [2920] => } [2921] => [2922] => /** [2923] => * Decodes encoded XML name characters back to their original form. [2924] => * [2925] => * @param string $name The encoded XML name to decode [2926] => * [2927] => * @return string The decoded name with _xNNN_ entities restored to characters [2928] => */ [2929] => function decodeXmlName(string $name): string { [2930] => return preg_replace_callback('/_x(\d+)_/',function ($matches) { [2931] => return chr(ltrim($matches[1], '0')); [2932] => }, $name); [2933] => } [2934] => [2935] => /** [2936] => * Prepares an array for XML/JSON conversion by encoding keys and preserving PHP types. [2937] => * [2938] => * Encodes attribute markers (@, @@), preserves empty arrays/objects, booleans, integers, and nulls [2939] => * using type suffix markers (--array, --boolean, --int, --null, --object). [2940] => * [2941] => * @param array $array The array to prepare [2942] => * [2943] => * @return array The prepared array with encoded keys [2944] => */ [2945] => function prepareJson(array $array): array { // <== [2946] => if (! is_array($array)) { [2947] => //return; [2948] => } [2949] => $helper = []; [2950] => [2951] => foreach ($array as $key => $value) { [2952] => if (is_numeric($key)) { [2953] => $key = 'n--' . $key; [2954] => } [2955] => [2956] => if ($key[0] == '@') { [2957] => if ($key[1] == '@') { [2958] => $key = substr($key, 2) . '--xmlattr'; [2959] => } else { [2960] => $key = substr($key, 1) . '--attr'; [2961] => } [2962] => } [2963] => [2964] => if (strpos($key, '@')) { [2965] => } [2966] => [2967] => //keep empty arrays as array type [2968] => if (is_object($value) && ! $value) { [2969] => $key .= '--object'; [2970] => } else { [2971] => if (is_array($value) && ! $value) { [2972] => $key .= '--array'; [2973] => } [2974] => } [2975] => [2976] => //keep boolean type [2977] => if (is_bool($value)) { [2978] => $key .= '--boolean'; [2979] => } [2980] => [2981] => //keep int type [2982] => if (is_int($value)) { [2983] => $key .= '--int'; [2984] => } [2985] => [2986] => //keep null type [2987] => if ($value === null) { [2988] => $key .= '--null'; [2989] => } [2990] => [2991] => $key = encodeXmlName(trim($key)); [2992] => [2993] => $helper[$key] = is_array($value) ? prepareJson($value) : $value; [2994] => } [2995] => [2996] => return $helper; [2997] => } [2998] => [2999] => /** [3000] => * Reverses prepareJson transformations to restore original array structure. [3001] => * [3002] => * Decodes type suffix markers, restores attribute prefixes, and converts numeric string keys back to integers. [3003] => * [3004] => * @param mixed $array The prepared array to reconstruct (passed by reference) [3005] => * @param bool $removeAttrs Whether to remove @attributes keys entirely [3006] => * [3007] => * @return array|null The reconstructed array, or null if input is not iterable [3008] => */ [3009] => function reconstructJson(mixed &$array, bool $removeAttrs = false): mixed { [3010] => if (! is_iterable($array)) { [3011] => return null; [3012] => } [3013] => $helper = []; [3014] => $array = (array)$array; [3015] => [3016] => foreach ($array as $key => $value) { [3017] => if ($removeAttrs && $key == '@attributes') { [3018] => continue; [3019] => } [3020] => [3021] => $key = decodeXmlName($key); [3022] => [3023] => if (substr_compare($key, '--array', -7, 7) === 0) { [3024] => $key = substr($key, 0, -7); [3025] => $value = (array)$value; [3026] => } else { [3027] => if ($value === null) { [3028] => $value = ''; [3029] => } [3030] => } [3031] => [3032] => if (substr_compare($key, '--boolean', -9, 9) === 0) { [3033] => $key = substr($key, 0, -9); [3034] => $value = (bool)$value; [3035] => } [3036] => [3037] => if (substr_compare($key, '--null', -6, 6) === 0) { [3038] => $key = substr($key, 0, -6); [3039] => $value = null; [3040] => } [3041] => [3042] => if (substr_compare($key, '--int', -5, 5) === 0) { [3043] => $key = substr($key, 0, -5); [3044] => $value = (int)$value; [3045] => } [3046] => [3047] => if (substr_compare($key, '--object', -8, 8) === 0) { [3048] => $key = substr($key, 0, -8); [3049] => $value = (object)$value; [3050] => } [3051] => [3052] => if (substr_compare($key, 'n--', 0, 3) === 0) { [3053] => $newkey = substr($key, 3); [3054] => [3055] => if (is_numeric($newkey)) { [3056] => $key = intval($newkey); [3057] => } [3058] => } [3059] => [3060] => if (substr_compare($key, '--xmlattr', -9, 9) === 0) { [3061] => $key = '@@' . substr($key, 0, -9); [3062] => } [3063] => [3064] => if (substr_compare($key, '--attr', -6, 6) === 0) { [3065] => $key = '@' . substr($key, 0, -6); [3066] => } [3067] => [3068] => $helper[$key] = (is_iterable($value)) ? reconstructJson($value, $removeAttrs) : $value; [3069] => } [3070] => [3071] => return $helper; [3072] => } [3073] => [3074] => /** [3075] => * Returns a human-readable error message for a PHP file upload error code. [3076] => * [3077] => * @param int $errorCode One of the UPLOAD_ERR_* constants [3078] => * [3079] => * @return string The translated error message [3080] => */ [3081] => function fileUploadErrMessage(int $errorCode): string { [3082] => return match ($errorCode) { [3083] => //UPLOAD_ERR_OK => __('Upload ok'), [3084] => UPLOAD_ERR_NO_FILE => __('No file sent'), [3085] => UPLOAD_ERR_PARTIAL => __('The uploaded file was only partially uploaded'), [3086] => UPLOAD_ERR_NO_TMP_DIR => __('Missing a temporary folder'), [3087] => UPLOAD_ERR_CANT_WRITE => __('Failed to write file to disk'), [3088] => UPLOAD_ERR_EXTENSION => __('A PHP extension stopped the file upload'), [3089] => UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => __('Exceeded filesize limit'), [3090] => default => __('Unknown errors'), [3091] => }; [3092] => } ) )