diff --git a/resources/classes/array_order.php b/resources/classes/array_order.php index 142193e22a..7fcfc48461 100644 --- a/resources/classes/array_order.php +++ b/resources/classes/array_order.php @@ -6,23 +6,42 @@ class array_order { var $backwards = false; var $numeric = false; + /** + * Sorts the provided array based on the specified fields. + * + * If no fields are provided, sorts in default order. If numeric sorting is enabled, + * uses the numericCompare method for comparison; otherwise, uses the stringCompare method. + * + * @param array $array The array to be sorted + * + * @return array The sorted array + */ function sort() { $args = func_get_args(); $array = $args[0]; - if (!$array) return array(); + if (!$array) return []; $this->sort_fields = array_slice($args, 1); if (!$this->sort_fields) return $array(); if ($this->numeric) { - usort($array, array($this, 'numericCompare')); + usort($array, [$this, 'numericCompare']); } else { - usort($array, array($this, 'stringCompare')); + usort($array, [$this, 'stringCompare']); } return $array; } + /** + * Compares two values based on a specified set of sort fields. + * + * @param array $a The first value to compare. + * @param array $b The second value to compare. + * + * @return int A negative integer if the first value is less than the second, a positive integer if the first value + * is greater than the second, and zero if they are equal. + */ function numericCompare($a, $b) { - foreach($this->sort_fields as $sort_field) { + foreach ($this->sort_fields as $sort_field) { if ($a[$sort_field] == $b[$sort_field]) { continue; } @@ -31,8 +50,17 @@ class array_order { return 0; } + /** + * Compares two strings according to the specified sort fields. + * + * @param string $a The first string to compare. + * @param string $b The second string to compare. + * + * @return int A negative integer if $a is less than $b, a positive integer if $a is greater than $b, + * and 0 if the strings are equal according to the specified sort fields. + */ function stringCompare($a, $b) { - foreach($this->sort_fields as $sort_field) { + foreach ($this->sort_fields as $sort_field) { $cmp_result = strcasecmp($a[$sort_field], $b[$sort_field]); if ($cmp_result == 0) continue; return ($this->backwards ? -$cmp_result : $cmp_result); @@ -40,7 +68,6 @@ class array_order { return 0; } } + //$order = new array_order(); //$registrations = $order->sort($registrations, 'domain', 'user'); - -?> \ No newline at end of file diff --git a/resources/classes/auto_loader.php b/resources/classes/auto_loader.php index 03526acf50..98c1b9a31b 100644 --- a/resources/classes/auto_loader.php +++ b/resources/classes/auto_loader.php @@ -38,38 +38,41 @@ class auto_loader { const CLASSES_FILE = 'autoloader_cache.php'; const INTERFACES_KEY = "autoloader_interfaces"; const INTERFACES_FILE = "autoloader_interface_cache.php"; - - private $classes; - - /** - * Tracks the APCu extension for caching to RAM drive across requests - * @var bool - */ - private $apcu_enabled; - /** * Cache path and file name for classes + * * @var string */ private static $classes_file = null; - + /** + * Cache path and file name for interfaces + * + * @var string + */ + private static $interfaces_file = null; + private $classes; + /** + * Tracks the APCu extension for caching to RAM drive across requests + * + * @var bool + */ + private $apcu_enabled; /** * Maps interfaces to classes + * * @var array */ private $interfaces; - /** * @var array */ private $traits; /** - * Cache path and file name for interfaces - * @var string + * Initializes the class and sets up caching mechanisms. + * + * @param bool $disable_cache If true, disables cache usage. Defaults to false. */ - private static $interfaces_file = null; - public function __construct($disable_cache = false) { //set if we can use RAM cache @@ -93,130 +96,14 @@ class auto_loader { $this->update_cache(); } //register this object to load any unknown classes - spl_autoload_register(array($this, 'loader')); + spl_autoload_register([$this, 'loader']); } /** - * The loader is set to private because only the PHP engine should be calling this method - * @param string $class_name The class name that needs to be loaded - * @return bool True if the class is loaded or false when the class is not found - * @access private + * Loads the class cache from various sources. + * + * @return bool True if the cache is loaded successfully, false otherwise. */ - private function loader($class_name): bool { - - //sanitize the class name - $class_name = preg_replace('[^a-zA-Z0-9_]', '', $class_name); - - //find the path using the class_name as the key in the classes array - if (isset($this->classes[$class_name])) { - //include the class or interface - include_once $this->classes[$class_name]; - - //return boolean - return true; - } - - //Smarty has it's own autoloader so reject the request - if ($class_name === 'Smarty_Autoloader') { - return false; - } - - //cache miss - self::log(LOG_WARNING, "class '$class_name' not found in cache"); - - //set project path using magic dir constant - $project_path = dirname(__DIR__, 2); - - //build the search path array - $search_path[] = glob($project_path . "/resources/interfaces/" . $class_name . ".php"); - $search_path[] = glob($project_path . "/resources/traits/" . $class_name . ".php"); - $search_path[] = glob($project_path . "/resources/classes/" . $class_name . ".php"); - $search_path[] = glob($project_path . "/*/*/resources/interfaces/" . $class_name . ".php"); - $search_path[] = glob($project_path . "/*/*/resources/traits/" . $class_name . ".php"); - $search_path[] = glob($project_path . "/*/*/resources/classes/" . $class_name . ".php"); - - //fix class names in the plugins directory prefixed with 'plugin_' - if (str_starts_with($class_name, 'plugin_')) { - $class_name = substr($class_name, 7); - } - $search_path[] = glob($project_path . "/core/authentication/resources/classes/plugins/" . $class_name . ".php"); - - //collapse all entries to only the matched entry - $matches = array_filter($search_path); - if (!empty($matches)) { - $path = array_pop($matches)[0]; - - //include the class, interface, or trait - include_once $path; - - //inject the class in to the array - $this->classes[$class_name] = $path; - - //update the cache with new classes - $this->update_cache(); - - //return boolean - return true; - } - - //send to syslog when debugging - self::log(LOG_ERR, "class '$class_name' not found name"); - - //return boolean - return false; - } - - /** - * Update the auto loader - */ - public function update() { - self::clear_cache(); - $this->reload_classes(); - $this->update_cache(); - } - - public function update_cache(): bool { - //guard against writing an empty file - if (empty($this->classes)) { - return false; - } - - //update RAM cache when available - if ($this->apcu_enabled) { - $classes_cached = apcu_store(self::CLASSES_KEY, $this->classes); - $interfaces_cached = apcu_store(self::INTERFACES_KEY, $this->interfaces); - //do not save to drive when we are using apcu - if ($classes_cached && $interfaces_cached) - return true; - } - - //export the classes array using PHP engine - $classes_array = var_export($this->classes, true); - - //put the array in a form that it can be loaded directly to an array - $class_result = file_put_contents(self::$classes_file, "interfaces, true); - - //put the array in a form that it can be loaded directly to an array - $interface_result = file_put_contents(self::$interfaces_file, "classes = []; $this->interfaces = []; @@ -251,6 +138,15 @@ class auto_loader { return (!empty($this->classes) && !empty($this->interfaces)); } + /** + * Reloads classes and interfaces from the project's resources. + * + * This method scans all PHP files in the specified locations, parses their contents, + * and updates the internal storage of classes and interfaces. It also processes + * implementation relationships between classes and interfaces. + * + * @return void + */ public function reload_classes() { //set project path using magic dir constant $project_path = dirname(__DIR__, 2); @@ -350,42 +246,85 @@ class auto_loader { } /** - * Returns a list of classes loaded by the auto_loader. If no classes have been loaded an empty array is returned. - * @return array List of classes loaded by the auto_loader or empty array + * Updates the cache by writing the classes and interfaces to files on disk. + * + * @return bool True if the update was successful, false otherwise */ - public function get_class_list(): array { - if (!empty($this->classes)) { - return $this->classes; + public function update_cache(): bool { + //guard against writing an empty file + if (empty($this->classes)) { + return false; } - return []; + + //update RAM cache when available + if ($this->apcu_enabled) { + $classes_cached = apcu_store(self::CLASSES_KEY, $this->classes); + $interfaces_cached = apcu_store(self::INTERFACES_KEY, $this->interfaces); + //do not save to drive when we are using apcu + if ($classes_cached && $interfaces_cached) + return true; + } + + //export the classes array using PHP engine + $classes_array = var_export($this->classes, true); + + //put the array in a form that it can be loaded directly to an array + $class_result = file_put_contents(self::$classes_file, "interfaces, true); + + //put the array in a form that it can be loaded directly to an array + $interface_result = file_put_contents(self::$interfaces_file, "classes) || empty($this->interfaces)) { - return []; + private static function log(int $level, string $message): void { + if (filter_var($_REQUEST['debug'] ?? false, FILTER_VALIDATE_BOOLEAN) || filter_var(getenv('DEBUG') ?? false, FILTER_VALIDATE_BOOLEAN)) { + openlog("PHP", LOG_PID | LOG_PERROR, LOG_LOCAL0); + syslog($level, "[auto_loader] " . $message); + closelog(); } - //check if we have an interface with that name - if (!empty($this->interfaces[$interface_name])) { - //return the list of classes associated with that interface - return $this->interfaces[$interface_name]; - } - //interface is not implemented by any classes - return []; } - public function get_interfaces(): array { - if (!empty($this->interfaces)) { - return $this->interfaces; - } - return []; + /** + * Main method used to update internal state by clearing cache, reloading classes and updating cache. + * + * @return void + * @see \auto_loader::clear_cache() + * @see \auto_loader::reload_classes() + * @see \auto_loader::update_cache() + */ + public function update() { + self::clear_cache(); + $this->reload_classes(); + $this->update_cache(); } + /** + * Clears the cache of stored classes and interfaces. + * + * @return void + */ public static function clear_cache() { //check for apcu cache @@ -426,11 +365,120 @@ class auto_loader { } } - private static function log(int $level, string $message): void { - if (filter_var($_REQUEST['debug'] ?? false, FILTER_VALIDATE_BOOL) || filter_var(getenv('DEBUG') ?? false, FILTER_VALIDATE_BOOL)) { - openlog("PHP", LOG_PID | LOG_PERROR, LOG_LOCAL0); - syslog($level, "[auto_loader] " . $message); - closelog(); + /** + * Returns a list of classes loaded by the auto_loader. If no classes have been loaded an empty array is returned. + * + * @return array List of classes loaded by the auto_loader or empty array + */ + public function get_class_list(): array { + if (!empty($this->classes)) { + return $this->classes; } + return []; + } + + /** + * Returns a list of classes implementing the interface + * + * @param string $interface_name + * + * @return array + */ + public function get_interface_list(string $interface_name): array { + //make sure we can return values + if (empty($this->classes) || empty($this->interfaces)) { + return []; + } + //check if we have an interface with that name + if (!empty($this->interfaces[$interface_name])) { + //return the list of classes associated with that interface + return $this->interfaces[$interface_name]; + } + //interface is not implemented by any classes + return []; + } + + /** + * Returns a list of all user defined interfaces that have been registered. + * + * @return array + */ + public function get_interfaces(): array { + if (!empty($this->interfaces)) { + return $this->interfaces; + } + return []; + } + + /** + * The loader is set to private because only the PHP engine should be calling this method + * + * @param string $class_name The class name that needs to be loaded + * + * @return bool True if the class is loaded or false when the class is not found + * @access private + */ + private function loader($class_name): bool { + + //sanitize the class name + $class_name = preg_replace('[^a-zA-Z0-9_]', '', $class_name); + + //find the path using the class_name as the key in the classes array + if (isset($this->classes[$class_name])) { + //include the class or interface + include_once $this->classes[$class_name]; + + //return boolean + return true; + } + + //Smarty has it's own autoloader so reject the request + if ($class_name === 'Smarty_Autoloader') { + return false; + } + + //cache miss + self::log(LOG_WARNING, "class '$class_name' not found in cache"); + + //set project path using magic dir constant + $project_path = dirname(__DIR__, 2); + + //build the search path array + $search_path[] = glob($project_path . "/resources/interfaces/" . $class_name . ".php"); + $search_path[] = glob($project_path . "/resources/traits/" . $class_name . ".php"); + $search_path[] = glob($project_path . "/resources/classes/" . $class_name . ".php"); + $search_path[] = glob($project_path . "/*/*/resources/interfaces/" . $class_name . ".php"); + $search_path[] = glob($project_path . "/*/*/resources/traits/" . $class_name . ".php"); + $search_path[] = glob($project_path . "/*/*/resources/classes/" . $class_name . ".php"); + + //fix class names in the plugins directory prefixed with 'plugin_' + if (str_starts_with($class_name, 'plugin_')) { + $class_name = substr($class_name, 7); + } + $search_path[] = glob($project_path . "/core/authentication/resources/classes/plugins/" . $class_name . ".php"); + + //collapse all entries to only the matched entry + $matches = array_filter($search_path); + if (!empty($matches)) { + $path = array_pop($matches)[0]; + + //include the class, interface, or trait + include_once $path; + + //inject the class in to the array + $this->classes[$class_name] = $path; + + //update the cache with new classes + $this->update_cache(); + + //return boolean + return true; + } + + //send to syslog when debugging + self::log(LOG_ERR, "class '$class_name' not found name"); + + //return boolean + return false; } } diff --git a/resources/classes/base2n.php b/resources/classes/base2n.php index e3f5eb6bb5..49c6aecaeb 100644 --- a/resources/classes/base2n.php +++ b/resources/classes/base2n.php @@ -22,8 +22,7 @@ * * @package binary-to-text-php */ -class base2n -{ +class base2n { protected $_chars; protected $_bitsPerCharacter; protected $_radix; @@ -36,21 +35,20 @@ class base2n /** * Constructor * - * @param integer $bitsPerCharacter Bits to use for each encoded character - * @param string $chars Base character alphabet - * @param boolean $caseSensitive To decode in a case-sensitive manner - * @param boolean $rightPadFinalBits How to encode last character - * @param boolean $padFinalGroup Add padding to end of encoded output - * @param string $padCharacter Character to use for padding + * @param integer $bitsPerCharacter Bits to use for each encoded character + * @param string $chars Base character alphabet + * @param boolean $caseSensitive To decode in a case-sensitive manner + * @param boolean $rightPadFinalBits How to encode last character + * @param boolean $padFinalGroup Add padding to end of encoded output + * @param string $padCharacter Character to use for padding * * @throws InvalidArgumentException for incompatible parameters */ public function __construct( $bitsPerCharacter, $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_', - $caseSensitive = TRUE, $rightPadFinalBits = FALSE, - $padFinalGroup = FALSE, $padCharacter = '=') - { + $caseSensitive = true, $rightPadFinalBits = false, + $padFinalGroup = false, $padCharacter = '=') { // Ensure validity of $chars if (!is_string($chars) || ($charLength = strlen($chars)) < 2) { throw new InvalidArgumentException('$chars must be a string of at least two characters'); @@ -68,7 +66,7 @@ class base2n $padCharFound = stripos($chars, $padCharacter[0]); } - if ($padCharFound !== FALSE) { + if ($padCharFound !== false) { throw new InvalidArgumentException('$padCharacter can not be a member of $chars'); } } @@ -94,9 +92,9 @@ class base2n $radix >>= 1; throw new InvalidArgumentException( - '$bitsPerCharacter can not be more than ' . $bitsPerCharacter - . ' given $chars length of ' . $charLength - . ' (max radix ' . $radix . ')'); + '$bitsPerCharacter can not be more than ' . $bitsPerCharacter + . ' given $chars length of ' . $charLength + . ' (max radix ' . $radix . ')'); } elseif ($bitsPerCharacter > 8) { // $bitsPerCharacter must not be greater than 8 @@ -106,23 +104,23 @@ class base2n $radix = 1 << $bitsPerCharacter; } - $this->_chars = $chars; - $this->_bitsPerCharacter = $bitsPerCharacter; - $this->_radix = $radix; + $this->_chars = $chars; + $this->_bitsPerCharacter = $bitsPerCharacter; + $this->_radix = $radix; $this->_rightPadFinalBits = $rightPadFinalBits; - $this->_padFinalGroup = $padFinalGroup; - $this->_padCharacter = $padCharacter[0]; - $this->_caseSensitive = $caseSensitive; + $this->_padFinalGroup = $padFinalGroup; + $this->_padCharacter = $padCharacter[0]; + $this->_caseSensitive = $caseSensitive; } /** * Encode a string * - * @param string $rawString Binary data to encode + * @param string $rawString Binary data to encode + * * @return string */ - public function encode($rawString) - { + public function encode($rawString) { // Unpack string into an array of bytes $bytes = unpack('C*', $rawString); $byteCount = count($bytes); @@ -132,11 +130,11 @@ class base2n $bitsRead = 0; $oldBits = 0; - $chars = $this->_chars; - $bitsPerCharacter = $this->_bitsPerCharacter; + $chars = $this->_chars; + $bitsPerCharacter = $this->_bitsPerCharacter; $rightPadFinalBits = $this->_rightPadFinalBits; - $padFinalGroup = $this->_padFinalGroup; - $padCharacter = $this->_padCharacter; + $padFinalGroup = $this->_padFinalGroup; + $padCharacter = $this->_padCharacter; $charsPerByte = 8 / $bitsPerCharacter; $encodedLength = $byteCount * $charsPerByte; @@ -159,7 +157,7 @@ class base2n if ($padFinalGroup) { // Array of the lowest common multiples of $bitsPerCharacter and 8, divided by 8 - $lcmMap = array(1 => 1, 2 => 1, 3 => 3, 4 => 1, 5 => 5, 6 => 3, 7 => 7, 8 => 1); + $lcmMap = [1 => 1, 2 => 1, 3 => 3, 4 => 1, 5 => 5, 6 => 3, 7 => 7, 8 => 1]; $bytesPerGroup = $lcmMap[$bitsPerCharacter]; $pads = $bytesPerGroup * $charsPerByte - ceil((strlen($rawString) % $bytesPerGroup) * $charsPerByte); $encodedString .= str_repeat($padCharacter, $pads); @@ -183,7 +181,7 @@ class base2n $bitsRead += $newBitCount; if ($oldBitCount) { - // Bits come from seperate bytes, add $oldBits to $bits + // Bits come from separate bytes, add $oldBits to $bits $bits = ($oldBits << $newBitCount) | $bits; } @@ -196,31 +194,31 @@ class base2n /** * Decode a string * - * @param string $encodedString Data to decode - * @param boolean $strict Returns NULL if $encodedString contains an undecodable character + * @param string $encodedString Data to decode + * @param boolean $strict Returns NULL if $encodedString contains an undecodable character + * * @return string */ - public function decode($encodedString, $strict = FALSE) - { + public function decode($encodedString, $strict = false) { if (!$encodedString || !is_string($encodedString)) { // Empty string, nothing to decode return ''; } - $chars = $this->_chars; - $bitsPerCharacter = $this->_bitsPerCharacter; - $radix = $this->_radix; + $chars = $this->_chars; + $bitsPerCharacter = $this->_bitsPerCharacter; + $radix = $this->_radix; $rightPadFinalBits = $this->_rightPadFinalBits; - $padFinalGroup = $this->_padFinalGroup; - $padCharacter = $this->_padCharacter; - $caseSensitive = $this->_caseSensitive; + $padFinalGroup = $this->_padFinalGroup; + $padCharacter = $this->_padCharacter; + $caseSensitive = $this->_caseSensitive; // Get index of encoded characters if ($this->_charmap) { $charmap = $this->_charmap; } else { - $charmap = array(); + $charmap = []; for ($i = 0; $i < $radix; $i++) { $charmap[$chars[$i]] = $i; @@ -293,12 +291,10 @@ class base2n } elseif ($strict) { // Unable to decode character; abort - return NULL; + return null; } } return $rawString; } } - -?> diff --git a/resources/classes/button.php b/resources/classes/button.php index 29d56facd9..f5eed5c389 100644 --- a/resources/classes/button.php +++ b/resources/classes/button.php @@ -25,123 +25,144 @@ Mark J Crane */ - class button { +class button { - public static $collapse = 'hide-md-dn'; + public static $collapse = 'hide-md-dn'; - public static function create($array) { - global $settings; + /** + * Creates a button element based on the provided array of attributes. + * + * @param array $array An array containing button attributes, such as type, name, value, id, label, title, onclick, + * etc. + * + * @return string The created button element as a string. + */ + public static function create($array) { + global $settings; - $button_icons = $settings->get('theme', 'button_icons', 'auto'); - //parse styles into array - if (!empty($array['style'])) { - $tmp = explode(';',$array['style']); - foreach ($tmp as $style) { - if (!empty($style)) { - $style = explode(':', $style); - if (is_array($style) && @sizeof($style) == 2) { - $styles[trim($style[0])] = trim($style[1]); - } - } - } - $array['style'] = $styles; - unset($styles); - } - //button: open - $button = ""; - //link - if (!empty($array['link'])) { - $anchor = " $value) { - if (substr_count($property, 'margin')) { - $styles .= $property.': '.$value.'; '; - } - } - $anchor .= $styles ? "style=".self::quote($styles)." " : null; - unset($styles); - } - $anchor .= isset($array['disabled']) && $array['disabled'] ? "class='disabled' onclick='return false;' " : null; - $anchor .= ">"; - $button = $anchor.$button.""; - } - return $button; + } + $array['style'] = $styles; + unset($styles); } - - private static function quote($value) { - return substr_count($value, "'") ? '"'.$value.'"' : "'".$value."'"; + //button: open + $button = ""; + //link + if (!empty($array['link'])) { + $anchor = " $value) { + if (substr_count($property, 'margin')) { + $styles .= $property . ': ' . $value . '; '; + } + } + $anchor .= $styles ? "style=" . self::quote($styles) . " " : null; + unset($styles); + } + $anchor .= isset($array['disabled']) && $array['disabled'] ? "class='disabled' onclick='return false;' " : null; + $anchor .= ">"; + $button = $anchor . $button . ""; + } + return $button; } + /** + * Quotes a value by surrounding it with single or double quotes based on whether it contains single quotes. + * + * @param string $value The value to be quoted. + * + * @return string The quoted value. + */ + private static function quote($value) { + return substr_count($value, "'") ? '"' . $value . '"' : "'" . $value . "'"; + } + + /** + * Escapes a URL by removing leading/trailing whitespace and encoding special characters. + * + * @param string $url The URL to escape. + * + * @return string The escaped URL. + */ + private static function escape_href(string $url): string { + // clear whitespace + $url = trim($url); + + return htmlspecialchars($url, ENT_QUOTES, 'UTF-8'); + } +} + /* //usage diff --git a/resources/classes/cache.php b/resources/classes/cache.php index 931f33bdae..5a03624e89 100644 --- a/resources/classes/cache.php +++ b/resources/classes/cache.php @@ -1,6 +1,5 @@ location = '/var/cache/fusionpbx'; } + /** + * Get a specific cache setting from the settings array. + * + * @param string $subcategory The subcategory of the cache setting to retrieve. + * + * @return mixed The value of the specified cache setting, or null if it does not exist. + */ private function setting($subcategory) { return $this->settings->get('cache', $subcategory); } /** - * Add a specific item in the cache - * @var string $key the cache id - * @var string $value string to be cached - */ - public function set($key, $value) { - - //change the delimiter - $key = str_replace(":", ".", $key); - - //save to memcache - if ($this->method === "memcache") { - //connect to event socket - $esl = event_socket::create(); - if ($esl === false) { - return false; - } - - //run the memcache - $command = "memcache set ".$key." ".$value; - $result = event_socket::api($command); - - } - - //save to the file cache - if ($this->method === "file") { - $result = file_put_contents($this->location . "/" . $key, $value); - } - - //return result - return $result; - } - - /** - * Get a specific item from the cache - * @var string $key cache id + * Retrieve the value associated with a given cache key. + * + * @param string $key The cache key to retrieve. Delimiter is automatically changed from ':' to '.'. + * + * @return mixed The cached value, or null if it does not exist. */ public function get($key) { //change the delimiter - $key = str_replace(":", ".", $key); + $key = str_replace(":", ".", $key); //cache method memcache - if ($this->method === "memcache") { - // connect to event socket - $esl = event_socket::create(); - if (!$esl->is_connected()) { - return false; - } - - //send a custom event - - //run the memcache - $command = "memcache get ".$key; - $result = event_socket::api($command); - + if ($this->method === "memcache") { + // connect to event socket + $esl = event_socket::create(); + if (!$esl->is_connected()) { + return false; } + //send a custom event + + //run the memcache + $command = "memcache get " . $key; + $result = event_socket::api($command); + + } + //get the file cache - if ($this->method === "file") { - if (file_exists($this->location . "/" . $key)) { - $result = file_get_contents($this->location . "/" . $key); - } + if ($this->method === "file") { + if (file_exists($this->location . "/" . $key)) { + $result = file_get_contents($this->location . "/" . $key); } + } //return result - return $result ?? null; + return $result ?? null; } /** - * Delete a specific item from the cache - * @var string $key cache id + * Set a value in the cache based on the cache type in global default settings. + * + * Cache location is based on the global default setting for either "memcache" or "file". + * + * @param string $key The key of the value to set. + * @param mixed $value The value to store. + * + * @return mixed When location is "file" the return value is in bytes written or null. When location is "memcache" + * return value is the return value from the switch socket response or false. + */ + public function set($key, $value) { + + //change the delimiter + $key = str_replace(":", ".", $key); + + //save to memcache + if ($this->method === "memcache") { + //connect to event socket + $esl = event_socket::create(); + if ($esl === false) { + return false; + } + + //run the memcache + $command = "memcache set " . $key . " " . $value; + $result = event_socket::api($command); + + } + + //save to the file cache + if ($this->method === "file") { + $result = file_put_contents($this->location . "/" . $key, $value); + } + + //return result + return $result; + } + + /** + * Delete a single cache key. + * + * @param string $key The cache key to delete + * + * @return bool When cache type is "memcache" false is returned on failure otherwise no value is returned */ public function delete($key) { //debug information - if ($this->syslog === "true") { - openlog("fusionpbx", LOG_PID | LOG_PERROR, LOG_USER); - syslog(LOG_WARNING, "debug: cache: [key: ".$key.", script: ".$_SERVER['SCRIPT_NAME'].", line: ".__line__."]"); - closelog(); - } + if ($this->syslog === "true") { + openlog("fusionpbx", LOG_PID | LOG_PERROR, LOG_USER); + syslog(LOG_WARNING, "debug: cache: [key: " . $key . ", script: " . $_SERVER['SCRIPT_NAME'] . ", line: " . __line__ . "]"); + closelog(); + } //cache method memcache - if ($this->method === "memcache") { - //connect to event socket - $esl = event_socket::create(); - if ($esl === false) { - return false; - } - - //send a custom event - $event = "sendevent CUSTOM\n"; - $event .= "Event-Name: CUSTOM\n"; - $event .= "Event-Subclass: fusion::memcache\n"; - $event .= "API-Command: memcache\n"; - $event .= "API-Command-Argument: delete ".$key."\n"; - event_socket::command($event); - - //run the memcache - $command = "memcache delete ".$key; - $result = event_socket::api($command); - + if ($this->method === "memcache") { + //connect to event socket + $esl = event_socket::create(); + if ($esl === false) { + return false; } + //send a custom event + $event = "sendevent CUSTOM\n"; + $event .= "Event-Name: CUSTOM\n"; + $event .= "Event-Subclass: fusion::memcache\n"; + $event .= "API-Command: memcache\n"; + $event .= "API-Command-Argument: delete " . $key . "\n"; + event_socket::command($event); + + //run the memcache + $command = "memcache delete " . $key; + $result = event_socket::api($command); + + } + //cache method file - if ($this->method === "file") { - //change the delimiter - $key = str_replace(":", ".", $key); + if ($this->method === "file") { + //change the delimiter + $key = str_replace(":", ".", $key); - //connect to event socket - $esl = event_socket::create(); - if ($esl === false) { - return false; - } - - //send a custom event - $event = "sendevent CUSTOM\n"; - $event .= "Event-Name: CUSTOM\n"; - $event .= "Event-Subclass: fusion::file\n"; - $event .= "API-Command: cache\n"; - $event .= "API-Command-Argument: delete ".$key."\n"; - event_socket::command($event); - - //remove the local files - foreach (glob($this->location . "/" . $key) as $file) { - if (file_exists($file)) { - unlink($file); - } - if (file_exists($file)) { - unlink($file . ".tmp"); - } - } + //connect to event socket + $esl = event_socket::create(); + if ($esl === false) { + return false; } + //send a custom event + $event = "sendevent CUSTOM\n"; + $event .= "Event-Name: CUSTOM\n"; + $event .= "Event-Subclass: fusion::file\n"; + $event .= "API-Command: cache\n"; + $event .= "API-Command-Argument: delete " . $key . "\n"; + event_socket::command($event); + + //remove the local files + foreach (glob($this->location . "/" . $key) as $file) { + if (file_exists($file)) { + unlink($file); + } + if (file_exists($file)) { + unlink($file . ".tmp"); + } + } + } + } /** - * Delete the entire cache + * Flushes the cache based on the current method setting. + * + * @return string|false The result of the flush operation, or false if an error occurred. */ public function flush() { //debug information - if ($this->syslog === "true") { - openlog("fusionpbx", LOG_PID | LOG_PERROR, LOG_USER); - syslog(LOG_WARNING, "debug: cache: [flush: all, script: ".$_SERVER['SCRIPT_NAME'].", line: ".__line__."]"); - closelog(); - } + if ($this->syslog === "true") { + openlog("fusionpbx", LOG_PID | LOG_PERROR, LOG_USER); + syslog(LOG_WARNING, "debug: cache: [flush: all, script: " . $_SERVER['SCRIPT_NAME'] . ", line: " . __line__ . "]"); + closelog(); + } //check for apcu extension - if (function_exists('apcu_enabled') && apcu_enabled()) { - //flush everything - apcu_clear_cache(); - } + if (function_exists('apcu_enabled') && apcu_enabled()) { + //flush everything + apcu_clear_cache(); + } //remove the autoloader file cache - if (file_exists(sys_get_temp_dir() . '/' . auto_loader::CLASSES_FILE)) { - @unlink(sys_get_temp_dir() . '/' . auto_loader::CLASSES_FILE); - } + if (file_exists(sys_get_temp_dir() . '/' . auto_loader::CLASSES_FILE)) { + @unlink(sys_get_temp_dir() . '/' . auto_loader::CLASSES_FILE); + } //cache method memcache - if ($this->method === "memcache") { - // connect to event socket - $esl = event_socket::create(); - if ($esl === false) { - return false; - } - - //send a custom event - $event = "sendevent CUSTOM\n"; - $event .= "Event-Name: CUSTOM\n"; - $event .= "Event-Subclass: fusion::memcache\n"; - $event .= "API-Command: memcache\n"; - $event .= "API-Command-Argument: flush\n"; - event_socket::command($event); - - //run the memcache - $command = "memcache flush"; - $result = event_socket::api($command); - + if ($this->method === "memcache") { + // connect to event socket + $esl = event_socket::create(); + if ($esl === false) { + return false; } + //send a custom event + $event = "sendevent CUSTOM\n"; + $event .= "Event-Name: CUSTOM\n"; + $event .= "Event-Subclass: fusion::memcache\n"; + $event .= "API-Command: memcache\n"; + $event .= "API-Command-Argument: flush\n"; + event_socket::command($event); + + //run the memcache + $command = "memcache flush"; + $result = event_socket::api($command); + + } + //cache method file - if ($this->method === "file") { - // connect to event socket - $esl = event_socket::create(); - if ($esl === false) { - return false; - } - - //send a custom event - $event = "sendevent CUSTOM\n"; - $event .= "Event-Name: CUSTOM\n"; - $event .= "Event-Subclass: fusion::file\n"; - $event .= "API-Command: cache\n"; - $event .= "API-Command-Argument: flush\n"; - event_socket::command($event); - - //remove the cache - recursive_delete($this->location); - - //set message - $result = '+OK cache flushed'; + if ($this->method === "file") { + // connect to event socket + $esl = event_socket::create(); + if ($esl === false) { + return false; } + //send a custom event + $event = "sendevent CUSTOM\n"; + $event .= "Event-Name: CUSTOM\n"; + $event .= "Event-Subclass: fusion::file\n"; + $event .= "API-Command: cache\n"; + $event .= "API-Command-Argument: flush\n"; + event_socket::command($event); + + //remove the cache + recursive_delete($this->location); + + //set message + $result = '+OK cache flushed'; + } + //return result - return $result; + return $result; } } - -?> diff --git a/resources/classes/captcha.php b/resources/classes/captcha.php index 40b6eca03b..d6a976d10c 100644 --- a/resources/classes/captcha.php +++ b/resources/classes/captcha.php @@ -32,20 +32,32 @@ class captcha { /** - * Called when the object is created - */ + * Called when the object is created + */ public $code; /** - * Class constructor - */ + * Class constructor + */ public function __construct() { } /** - * Create the captcha image - * @var string $code + * Returns a Base64 encoded version of the CAPTCHA image. + * + * @return string The Base64 encoded CAPTCHA image data. + */ + public function image_base64() { + return base64_encode($this->image_captcha()); + } + + /** + * Generates a CAPTCHA image. + * + * Requires the object property code for the text to create + * + * @return string The CAPTCHA image buffer. */ public function image_captcha() { @@ -54,25 +66,25 @@ class captcha { $text = $this->code; // Set the font path - $font_path = $_SERVER["DOCUMENT_ROOT"]."/resources/captcha/fonts"; + $font_path = $_SERVER["DOCUMENT_ROOT"] . "/resources/captcha/fonts"; // Array of fonts //$fonts[] = 'ROUGD.TTF'; //$fonts[] = 'Zebra.ttf'; //$fonts[] = 'hanshand.ttf'; - $fonts = glob($font_path.'/*.[tT][tT][fF]'); + $fonts = glob($font_path . '/*.[tT][tT][fF]'); //print_r($fonts); //exit; // Randomize the fonts srand(); - $random = (rand()%count($fonts)); + $random = (rand() % count($fonts)); //$font = $font_path.'/'.$fonts[$random]; $font = $fonts[$random]; // Set the font size $font_size = 16; - if(@$_GET['fontsize']) { + if (@$_GET['fontsize']) { $font_size = $_GET['fontsize']; } @@ -109,15 +121,14 @@ class captcha { } /** - * return the image in base64 - */ - public function image_base64() { - return base64_encode($this->image_captcha()); - } - - /** - * Get the image size - * @var string $value string image size + * Calculates the bounding box of a text in an image. + * + * @param int $size The size of the font. + * @param float $angle The angle of rotation. + * @param string $font The path to the font file. + * @param string $text The text to be rendered. + * + * @return array An array containing the bounding box coordinates (x, y, w, h). */ private function image_size($size, $angle, $font, $text) { $dummy = imagecreate(1, 1); @@ -135,5 +146,3 @@ $captcha->code = 'abcdefg'; $image_base64 = $captcha->base64(); echo "\n"; */ - -?> diff --git a/resources/classes/command_option.php b/resources/classes/command_option.php index 622e3b8974..573758be55 100644 --- a/resources/classes/command_option.php +++ b/resources/classes/command_option.php @@ -28,6 +28,7 @@ /** * Container object for creating command line options when creating a service + * * @author Tim Fry */ class command_option { @@ -52,9 +53,11 @@ class command_option { } /** - * A factory method to create a new command_option - * @param type $options - * @return command_option + * Creates a new instance of CommandOption with automatically assigned properties. + * + * @param array $options Key/Value pairs to assign as properties on the new instance. + * + * @return command_option Returns a populated instance of command_option. */ public static function new(...$options): command_option { $obj = new command_option(); @@ -67,6 +70,15 @@ class command_option { } // used to parse object values when created + + /** + * Recursively parses the provided options array and applies its values to the given object. + * + * @param mixed $obj The object whose properties will be updated with the parsed options + * @param array $options The associative array containing the options to parse and apply + * + * @return void This method does not return a value, it updates the provided object instead. + */ private static function parse_options($obj, $options) { foreach ($options as $key => $value) { if (is_array($value)) { @@ -81,9 +93,63 @@ class command_option { } } + /** + * Appends the callback function to the array of existing callback functions + * + * @param string|null $function When function param is set, the callback function will be appended to the list of + * functions. When called without a param, the array will be returned of current + * callbacks. + * + * @return $this|array Returns the array of callbacks if no parameters passed or this object when appending a + * callback + */ + public function callback(?string $function = null) { + if ($function !== null) { + $this->functions += [$function]; + return $this; + } + return $this->functions; + } + + /** + * Appends the callback function to the array of existing callback functions + * + * @param string|null $function When function param is set, the callback function will be appended to the list of + * functions. When called without a param, the array will be returned of current + * callbacks. + * + * @return $this|array Returns the array of callbacks if no parameters passed or this object when appending a + * callback + */ + public function function_append(?string $function = null) { + if ($function !== null) { + $this->functions += [$function]; + return $this; + } + return $this->functions; + } + + /** + * Converts the current object to an array. + * + * @return array The array representation of the current object, containing + * information about options and functions. + */ + public function to_array(): array { + $array['short_option'] = $this->short_option(); + $array['long_option'] = $this->long_option(); + $array['description'] = $this->description(); + $array['short_description'] = $this->short_description(); + $array['long_description'] = $this->long_description(); + $array['functions'] = $this->functions(); + return $array; + } + /** * Sets or returns the short option value + * * @param string|null $short_option + * * @return $this */ public function short_option(?string $short_option = null) { @@ -96,7 +162,9 @@ class command_option { /** * Sets or returns the long option value + * * @param string|null $long_option + * * @return $this */ public function long_option(?string $long_option = null) { @@ -109,7 +177,9 @@ class command_option { /** * Set the general description + * * @param string|null $description + * * @return $this */ public function description(?string $description = null) { @@ -122,7 +192,10 @@ class command_option { /** * Sets or returns the short_description. If short_description is empty then the short_option is used as a default. - * @param string|null $short_description When parameter is null, it returns the currently set value. When not null the short description is set to the passed value. + * + * @param string|null $short_description When parameter is null, it returns the currently set value. When not null + * the short description is set to the passed value. + * * @return $this */ public function short_description(?string $short_description = null) { @@ -145,8 +218,11 @@ class command_option { /** * Sets or returns the long_description. If long_description is empty then the long_option is used as a default. - * @param string|null $long_description When parameter is null, it returns the currently set value. When not null the long description is set to the passed value. - * @return $this + * + * @param string|null $long_description When parameter is null, it returns the currently set value. When not null + * the long description is set to the passed value. + * + * @return self|string */ public function long_description(?string $long_description = null) { if ($long_description !== null) { @@ -167,9 +243,13 @@ class command_option { } /** - * Adds an array of callback functions replacing the existing callback functions - * @param array|null $functions - * @return $this + * Sets or retrieves the array of callback functions + * + * @param array|null $functions When functions param is set, the array will be assigned to the list of callbacks. + * When called without a parameter, the current array of callbacks will be returned. + * + * @return $this|array Returns the array of callbacks if no parameters passed or this object when setting a new + * array */ public function functions(?array $functions = null) { if ($functions !== null) { @@ -178,46 +258,6 @@ class command_option { } return $this->functions; } - - /** - * Appends the callback function to the array of existing callback functions - * @param string|null $function When function param is set, the callback function will be appended to the list of functions. When called without a param, the array will be returned of current callbacks. - * @return $this|array Returns the array of callbacks if no parameters passed or this object when appending a callback - */ - public function callback(?string $function = null) { - if ($function !== null) { - $this->functions += [$function]; - return $this; - } - return $this->functions; - } - - /** - * Appends the callback function to the array of existing callback functions - * @param string|null $function - * @return $this - */ - public function function_append(?string $function = null) { - if ($function !== null) { - $this->functions += [$function]; - return $this; - } - return $this->functions; - } - - /** - * Returns the array structure required for service - * @return array - */ - public function to_array(): array { - $array['short_option'] = $this->short_option(); - $array['long_option'] = $this->long_option(); - $array['description'] = $this->description(); - $array['short_description'] = $this->short_description(); - $array['long_description'] = $this->long_description(); - $array['functions'] = $this->functions(); - return $array; - } } /* Examples diff --git a/resources/classes/config.php b/resources/classes/config.php index 80f8aea0bb..52ede00e29 100644 --- a/resources/classes/config.php +++ b/resources/classes/config.php @@ -2,39 +2,48 @@ /** * config class loads configuration from the file system - * @param string $db_type Type of database - * @param string $db_driver Alias of type - * @param string $db_host Host to connect to - * @param string $db_path Path of the database if it is file system based - * @param string $db_file File name of the database if it is file system based - * @param string $db_port Port to connect to - * @param string $db_name Name of the database - * @param string $db_sslmode SSL Mode to use - * @param string $db_cert_authority The certificate authority - * @param string $db_secure If the database is using a secure connection - * @param string $db_username Username credentials to connect with - * @param string $db_password Password credentials to connect with - * @param string $config_path Configuration path currently in use - * @param string $config_file Configuration file currently in use + * + * @param string $db_type Type of database + * @param string $db_driver Alias of type + * @param string $db_host Host to connect to + * @param string $db_path Path of the database if it is file system based + * @param string $db_file File name of the database if it is file system based + * @param string $db_port Port to connect to + * @param string $db_name Name of the database + * @param string $db_sslmode SSL Mode to use + * @param string $db_cert_authority The certificate authority + * @param string $db_secure If the database is using a secure connection + * @param string $db_username Username credentials to connect with + * @param string $db_password Password credentials to connect with + * @param string $config_path Configuration path currently in use + * @param string $config_file Configuration file currently in use * @param string $config_path_and_filename Full path and configuration file currently in use - * @internal the @param statements are used because they match the magic __get function that allows those to be accessed publicly + * + * @internal the @param statements are used because they match the magic __get function that allows those to be + * accessed publicly */ final class config { // Full path and filename of config.conf - private $file; - - // The internal array that holds the configuration in the config.conf file - private $configuration; - /** * Configuration object used to hold a single instance + * * @var array */ public static $config = null; + // The internal array that holds the configuration in the config.conf file + private $file; + private $configuration; + /** - * Loads the framework configuration file + * Initializes a new instance of the class with an optional configuration file. + * + * If no file is provided, it will attempt to locate one using the `find()` method. + * + * @param string $file The path to the configuration file (optional). + * + * @return void */ public function __construct(string $file = '') { @@ -60,78 +69,45 @@ final class config { } /** - * Magic method to allow backward compatibility for variables such as db_type. - *

This will allow using config object with the syntax of:
- * $config = new config();
- * $db_type = $config->db_type;

- *

Note:
- * The InvalidArgumentException is thrown if there is no such variable accessed such as:
- * $config = new config();
- * $db_function = $config->db_function(); - *

- *

This is ensure that any invalid code is detected and fixed.

- * @param string $name Name of the object property - * @return string Returns the value as a string + * Finds and returns the path of the configuration file. + * + * Find tries to look for the config.conf file in the following locations: /etc/fusionpbx, /usr/local/etc/fusionpbx. + * When unsuccessful it will then search for the config.php file in the same locations. Last, find will search the + * SystemDrive folder for Windows operating systems trying first for config.conf and then config.php. + * + * @return string path to the configuration file */ - public function __get(string $name): string { - switch($name) { - case 'db_type': - case 'db_driver': - return $this->configuration['database.0.type'] ?? ''; - case 'db_path': - case 'path': - return $this->configuration['database.0.path'] ?? ''; - case 'db_host': - return $this->configuration['database.0.host'] ?? ''; - case 'db_port': - return $this->configuration['database.0.port'] ?? ''; - case 'db_name': - return $this->configuration['database.0.name'] ?? ''; - case 'db_sslmode': - return $this->configuration['database.0.sslmode'] ?? 'prefer'; - case 'db_cert_authority': - return $this->configuration['database.0.cert_authority'] ?? ''; - case 'db_secure': - return $this->configuration['database.0.secure'] ?? 'false'; - case 'db_username': - case 'username': - return $this->configuration['database.0.username'] ?? ''; - case 'db_password': - case 'password': - return $this->configuration['database.0.password'] ?? ''; - case 'db_file': - return $this->configuration['database.0.file'] ?? ''; - case 'config_path': - return $this->path(); - case 'config_filename': - return $this->filename(); - case 'config_path_and_filename': - case 'config_file': - return $this->path_and_filename(); - default: - if (property_exists($this, $name)) { - return $this->{$name}; - } - elseif (array_key_exists($name, $this->configuration)) { - return $this->configuration[$name]; - } + public static function find(): string { + //define the file variable + $file = ""; + + //find the file + if (file_exists("/etc/fusionpbx/config.conf")) { + $file = "/etc/fusionpbx/config.conf"; + } elseif (file_exists("/usr/local/etc/fusionpbx/config.conf")) { + $file = "/usr/local/etc/fusionpbx/config.conf"; + } elseif (file_exists("/etc/fusionpbx/config.php")) { + $file = "/etc/fusionpbx/config.php"; + } elseif (file_exists("/usr/local/etc/fusionpbx/config.php")) { + $file = "/usr/local/etc/fusionpbx/config.php"; + } elseif (file_exists(getenv('SystemDrive') . DIRECTORY_SEPARATOR . 'ProgramData' . DIRECTORY_SEPARATOR . 'fusionpbx' . DIRECTORY_SEPARATOR . 'config.conf')) { + $file = getenv('SystemDrive') . DIRECTORY_SEPARATOR . 'ProgramData' . DIRECTORY_SEPARATOR . 'fusionpbx' . DIRECTORY_SEPARATOR . 'config.conf'; + } elseif (file_exists(dirname(__DIR__, 2) . "/resources/config.php")) { + //use the current web directory to find it as a last resort + $file = "/var/www/fusionpbx/resources/config.php"; } - return ""; + return $file; } /** - * Returns the string representation of the configuration file - * @return string configuration + * Reads and parses the configuration file. + * + * If the file has a .php extension, it will be included as PHP code. Otherwise, + * it will be parsed using the parse_ini_file function. + * The old properties from config.php are converted to the new standard. + * + * @return void */ - public function __toString(): string { - $string_builder = ""; - foreach ($this->configuration as $key => $value) { - $string_builder .= "$key = '$value'\n"; - } - return $string_builder; - } - - // loads the config.conf file public function read() { //check if include is needed @@ -183,27 +159,39 @@ final class config { //remove from the global namespace unset($db_type, $db_host, $db_port, $db_name, $db_username, $db_password, $db_sslmode, $db_secure, $db_cert_authority); - } - else { + } else { //save the loaded and parsed conf file to the object $this->configuration = parse_ini_file($this->file); } } - // set project paths if not already defined + // loads the config.conf file + + /** + * Defines project paths and sets internal server variables + * + * @return void + */ private function define_project_paths() { // Load the document root $doc_root = $this->get('document.root', '/var/www/fusionpbx'); $doc_path = $this->get('document.path', ''); //set the server variables and define project path constant if (!empty($doc_path)) { - if (!defined('PROJECT_PATH')) { define("PROJECT_PATH", $doc_path); } - if (!defined('PROJECT_ROOT')) { define("PROJECT_ROOT", $doc_root.'/'.$doc_path); } - } - else { - if (!defined('PROJECT_PATH')) { define("PROJECT_PATH", ''); } - if (!defined('PROJECT_ROOT')) { define("PROJECT_ROOT", $doc_root); } + if (!defined('PROJECT_PATH')) { + define("PROJECT_PATH", $doc_path); + } + if (!defined('PROJECT_ROOT')) { + define("PROJECT_ROOT", $doc_root . '/' . $doc_path); + } + } else { + if (!defined('PROJECT_PATH')) { + define("PROJECT_PATH", ''); + } + if (!defined('PROJECT_ROOT')) { + define("PROJECT_ROOT", $doc_root); + } } // internal definitions to the framework @@ -217,41 +205,15 @@ final class config { set_include_path(PROJECT_ROOT); } - /** - * Find the path to the config.conf file - * @var string $config_path - full path to the config.php file - */ - public static function find(): string { - //define the file variable - $file = ""; - - //find the file - if (file_exists("/etc/fusionpbx/config.conf")) { - $file = "/etc/fusionpbx/config.conf"; - } - elseif (file_exists("/usr/local/etc/fusionpbx/config.conf")) { - $file = "/usr/local/etc/fusionpbx/config.conf"; - } - elseif (file_exists("/etc/fusionpbx/config.php")) { - $file = "/etc/fusionpbx/config.php"; - } - elseif (file_exists("/usr/local/etc/fusionpbx/config.php")) { - $file = "/usr/local/etc/fusionpbx/config.php"; - } - elseif (file_exists(getenv('SystemDrive') . DIRECTORY_SEPARATOR . 'ProgramData' . DIRECTORY_SEPARATOR . 'fusionpbx' . DIRECTORY_SEPARATOR . 'config.conf')) { - $file = getenv('SystemDrive') . DIRECTORY_SEPARATOR . 'ProgramData' . DIRECTORY_SEPARATOR . 'fusionpbx' . DIRECTORY_SEPARATOR . 'config.conf'; - } - elseif (file_exists(dirname(__DIR__, 2) . "/resources/config.php")) { - //use the current web directory to find it as a last resort - $file = "/var/www/fusionpbx/resources/config.php"; - } - return $file; - } + // set project paths if not already defined /** * Get a configuration value using a key in the configuration file - * @param string $key Match key on the left hand side of the '=' in the config file. If $key is null the default value is returned + * + * @param string $key Match key on the left hand side of the '=' in the config file. If $key is null + * the default value is returned * @param string|null $default_value if no matching key is found, then this value will be returned + * * @return string|null returns a value in the config.conf file or an empty string */ public function get(string $key, ?string $default_value = ''): ?string { @@ -262,48 +224,103 @@ final class config { } /** - * Returns the config path or an empty string - * @return string + * Magic method to allow backward compatibility for variables such as db_type. + *

This will allow using config object with the syntax of:
+ * $config = new config();
+ * $db_type = $config->db_type;

+ *

Note:
+ * The InvalidArgumentException is thrown if there is no such variable accessed such as:
+ * $config = new config();
+ * $db_function = $config->db_function(); + *

+ *

This is ensure that any invalid code is detected and fixed.

+ * + * @param string $name Name of the object property + * + * @return string Returns the value as a string + */ + public function __get(string $name): string { + switch ($name) { + case 'db_type': + case 'db_driver': + return $this->configuration['database.0.type'] ?? ''; + case 'db_path': + case 'path': + return $this->configuration['database.0.path'] ?? ''; + case 'db_host': + return $this->configuration['database.0.host'] ?? ''; + case 'db_port': + return $this->configuration['database.0.port'] ?? ''; + case 'db_name': + return $this->configuration['database.0.name'] ?? ''; + case 'db_sslmode': + return $this->configuration['database.0.sslmode'] ?? 'prefer'; + case 'db_cert_authority': + return $this->configuration['database.0.cert_authority'] ?? ''; + case 'db_secure': + return $this->configuration['database.0.secure'] ?? 'false'; + case 'db_username': + case 'username': + return $this->configuration['database.0.username'] ?? ''; + case 'db_password': + case 'password': + return $this->configuration['database.0.password'] ?? ''; + case 'db_file': + return $this->configuration['database.0.file'] ?? ''; + case 'config_path': + return $this->path(); + case 'config_filename': + return $this->filename(); + case 'config_path_and_filename': + case 'config_file': + return $this->path_and_filename(); + default: + if (property_exists($this, $name)) { + return $this->{$name}; + } elseif (array_key_exists($name, $this->configuration)) { + return $this->configuration[$name]; + } + } + return ""; + } + + /** + * Returns the directory path of the configuration file. + * + * @return string the directory path of the configuration file */ public function path(): string { return dirname($this->file); } /** - * Returns the file name only of the configuration file - * @return string + * Returns the filename of the processed file. + * + * @return string filename */ public function filename(): string { return basename($this->file); } /** - * Returns the path and the file name - * @return string + * Returns the file's path and filename. + * + * @return string path and filename of the file */ public function path_and_filename(): string { return $this->file; } /** - * Returns if the config class has a loaded configuration or not - * @return bool True if configuration has loaded and false if it is empty - */ - public function is_empty(): bool { - return count($this->configuration) === 0; - } - - /** - * Returns the array of configuration settings - * @return array - */ - public function configuration(): array { - return $this->configuration; - } - - /** - * Ensures the configuration file is loaded only once - * @return config + * Returns a singleton instance of the configuration object + * + * If no file path is provided, loads the default configuration. + * Otherwise, attempts to load the specified file and returns the result. + * + * @param string $file The optional file path to load (default: ''). Note: If the configuration file is already + * loaded, the file provided will be ignored. + * + * @return config The loaded or default configuration object */ public static function load(string $file = ''): config { if (self::$config === null) { @@ -311,6 +328,37 @@ final class config { } return self::$config; } + + /** + * Returns the string representation of the configuration file + * + * @return string configuration + */ + public function __toString(): string { + $string_builder = ""; + foreach ($this->configuration as $key => $value) { + $string_builder .= "$key = '$value'\n"; + } + return $string_builder; + } + + /** + * Checks if the configuration is empty. + * + * @return bool true if the configuration is empty, false otherwise + */ + public function is_empty(): bool { + return count($this->configuration) === 0; + } + + /** + * Returns the current application configuration + * + * @return array configuration data + */ + public function configuration(): array { + return $this->configuration; + } } /* diff --git a/resources/classes/database.php b/resources/classes/database.php index 8cf1f64288..8c2857097d 100644 --- a/resources/classes/database.php +++ b/resources/classes/database.php @@ -25,100 +25,110 @@ Luis Daniel Lucio Quiroz */ -//define the database class +/** + * Database class + * + * @property $name Alias of app_name + */ class database { /** * - */ + */ const TABLE_PREFIX = "v_"; - + /** + * Stores the application built from the app_config files. + * + * @see $apps + * @access private + * @var array + */ + private static $apps = []; + /** + * Singleton type class + * + * @var database + */ + private static $database; /** * Database connection + * * @access private * @var PDO object */ public $db; - /** * Driver to use. + * * @access public * @var string Can be pgsql, mysql, sqlite, odbc */ public $driver; - /** * Alias of driver. + * * @access public + * @see $driver * @var string Can be pgsql, mysql, sqlite, odbc - * @see $driver */ public $type; - /** * Host for database connection + * * @access public * @var string host name or IP address. */ public $host; - /** * Port number + * * @access public * @var int 1025 - 65534 */ public $port; - /** * Database name + * * @access public * @var string */ public $db_name; - /** * Database security + * * @access public * @var boolean */ public $db_secure; - /** * Specifies the file name of the client SSL certificate + * * @access public * @var string full path */ public $db_cert_authority; - /** * Username used to connect + * * @access public * @var string */ public $username; - /** * Password used to connect + * * @access public * @var string */ public $password; - /** * Full path to file name. + * * @access public * @var string full path to file name */ - public $path; - - /** - * Table name. - * @access private - * @var string sanitized - */ - private $table; - - /** + public $path; //array +/** * Where clause(s) of an SQL statement. *

Array of arrays must be passed with each having the * following keys: @@ -127,17 +137,18 @@ class database { *

  • 'value' - Value being matched
  • *

    Example Usage:

    *

    $db->where['SearchTerm'] = ['name'=>'MyColumn','operator'=>'=','value'=>'MySearchTerm'

    - *

    $db->where['NextSearchTerm'] = ['name'=>'MyColumn','operator'=>'=','value'=>'MyOtherSearchTerm'

    + *

    $db->where['NextSearchTerm'] = + * ['name'=>'MyColumn','operator'=>'=','value'=>'MyOtherSearchTerm'

    *

    Below is equivalent to the above.

    *

    $db->where[0] = ['name'=>'MyColumn','operator'=>'=','value'=>'MyValue'

    *

    $db->where[1] = ['name'=>'MyColumn','operator'=>'=>','value'=>'MyValue'

    + * * @access public + * @see $order_by * @var array Two dimensional array of key value pairs - * @see $order_by */ public $where; //array - - /** +/** * Order By clause(s) of an SQL statement. *

    Array of arrays must be passed with each having the * following keys: @@ -146,155 +157,153 @@ class database { *

  • 'value' - Value being matched
  • *

    Example Usage:

    *

    $db->where['SearchTerm'] = ['name'=>'MyColumn','operator'=>'=','value'=>'MySearchTerm'

    - *

    $db->where['NextSearchTerm'] = ['name'=>'MyColumn','operator'=>'=','value'=>'MyOtherSearchTerm'

    + *

    $db->where['NextSearchTerm'] = + * ['name'=>'MyColumn','operator'=>'=','value'=>'MyOtherSearchTerm'

    *

    Below is equivalent to the above.

    *

    $db->where[0] = ['name'=>'MyColumn','operator'=>'=','value'=>'MyValue'

    *

    $db->where[1] = ['name'=>'MyColumn','operator'=>'=>','value'=>'MyValue'

    + * * @access private + * @see $where * @var array Two dimensional array of key value pairs - * @see $where */ - public $order_by; //array - + public $order_by; /** * Ascending or Descending order. + * * @var string * @access public */ public $order_type; - /** * Numerical value to limit returned results. + * * @var int Used for 'LIMIT' in SQL statement. * @access public */ public $limit; - /** * Numerical value to offset returned results. + * * @var int Used for 'OFFSET' in SQL statement. * @access public */ public $offset; - /** *

    Array of fields.

    *

    Fields are specified in 'name'=>'value' format. *

    Used by {@link database::add() } and {@link database::update() }

    + * * @access public + * @see database::add() + * @see database::update() * @var array Array of columns - * @see database::add() - * @see database::update() */ public $fields; - /** * Unknown property + * * @var unknown * @access public */ public $count; - /** * Unknown property + * * @var unknown * @access public */ public $sql; - - /** - *

    Stores the result from the most recent query. The type will be based on what was requested.

    - *

    NOTE: If an error occurred on the last query the result is set to an empty string.

    - * @var mixed - */ - private $result; - - /** - * Stores the application built from the app_config files. - * @var array - * @see $apps - * @access private - */ - private static $apps = []; - /** * Stores the application name making the request. + * * @var string App name making database request. * @access public */ public $name; - /** * Stores the application name making the request. - * @var string App name making database request. - * @see $app_uuid + * + * @see $app_uuid * @access public + * @var string App name making database request. */ public $app_name; - /** * Stores the application UUID making the request. - * @var string - * @see $app_name + * + * @see $app_name * @access public + * @var string */ public $app_uuid; - /** *

    Stores the domain UUID making the request.

    *

    This is defaulted to the Session domain UUID.

    + * * @access public - * @uses $this->domain_uuid
    Default value upon object creation + * @uses $this->domain_uuid
    Default value upon object creation * @var string Domain UUID making request. */ public $domain_uuid; - /** *

    Stores the user UUID making the request.

    *

    This is defaulted to the Session domain UUID.

    + * * @access public - * @uses $this->user_uuid
    Default value upon object creation + * @uses $this->user_uuid
    Default value upon object creation * @var string Domain UUID making request. */ public $user_uuid; - /** *

    Message for the query results.

    + * * @var array Contains the message array after a query * @access private */ public $message; - + /** + * SSL Mode used to connect to the database + * + * @var string prefer or verify-ca. Default is 'prefer' + */ + public $ssl_mode; + /** + * Table name. + * + * @access private + * @var string sanitized + */ + private $table; + /** + *

    Stores the result from the most recent query. The type will be based on what was requested.

    + *

    NOTE: If an error occurred on the last query the result is set to an empty string.

    + * + * @var mixed + */ + private $result; /** * Config object used to get the database connection params + * * @var config */ private $config; /** - * SSL Mode used to connect to the database - * @var string prefer or verify-ca. Default is 'prefer' - */ - public $ssl_mode; - - /** - * Singleton type class - * @var database - */ - private static $database; - - /** - * Called when the object is created - * @param array $params Optional + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $params An optional array of settings to override default values. Defaults to []. */ public function __construct(array $params = []) { //handle the config object if (isset($params['config'])) { $config = $params['config']; - } - else { - $config = new config(); + } else { + //use singleton config + $config = config::load(); } //driver and type point to the same value @@ -334,14 +343,366 @@ class database { } } + /** + *

    Connect to the database.

    + *

    Database driver must be set before calling connect.

    + *

    For types other than sqlite. Execution will stop on failure.

    + * + * @depends database::driver Alias of database::type. + * + */ + public function connect() { + + //get the database connection settings + //$db_type = $conf['database.0.type']; + //$db_host = $conf['database.0.host']; + //$db_port = $conf['database.0.port']; + //$db_name = $conf['database.0.name']; + //$db_username = $conf['database.0.username']; + //$db_password = $conf['database.0.password']; + + //debug info + //echo "db type:".$db_type."\n"; + //echo "db host:".$db_host."\n"; + //echo "db port:".$db_port."\n"; + //echo "db name:".$db_name."\n"; + //echo "db username:".$db_username."\n"; + //echo "db password:".$db_password."\n"; + //echo "db path:".$db_path."\n"; + //echo "\n"; + + //set defaults + if (!isset($this->driver) && isset($db_type)) { + $this->driver = $db_type; + } + if (!isset($this->type) && isset($db_type)) { + $this->type = $db_type; + } + if (!isset($this->host) && isset($db_host)) { + $this->host = $db_host; + } + if (!isset($this->port) && isset($db_port)) { + $this->port = $db_port; + } + if (!isset($this->db_name) && isset($db_name)) { + $this->db_name = $db_name; + } + if (!isset($this->db_secure) && isset($db_secure)) { + $this->db_secure = $db_secure; + } else { + $this->db_secure = false; + } + if (!isset($this->username) && isset($db_username)) { + $this->username = $db_username; + } + if (!isset($this->password) && isset($db_password)) { + $this->password = $db_password; + } + if (!isset($this->path) && isset($db_path)) { + $this->path = $db_path; + } + + if ($this->driver == "sqlite") { + if (empty($this->db_name)) { + $server_name = $_SERVER["SERVER_NAME"]; + $server_name = str_replace("www.", "", $server_name); + $db_name_short = $server_name; + $this->db_name = $server_name . '.db'; + } else { + $db_name_short = $this->db_name; + } + $this->path = realpath($this->path); + if (file_exists($this->path . '/' . $this->db_name)) { + //connect to the database + $this->db = new PDO('sqlite:' . $this->path . '/' . $this->db_name); //sqlite 3 + //PRAGMA commands + $this->db->query('PRAGMA foreign_keys = ON;'); + $this->db->query('PRAGMA journal_mode = wal;'); + //add additional functions to SQLite so that they are accessible inside SQL + //bool PDO::sqliteCreateFunction ( string function_name, callback callback [, int num_args] ) + $this->db->sqliteCreateFunction('md5', 'php_md5', 1); + $this->db->sqliteCreateFunction('unix_timestamp', 'php_unix_timestamp', 1); + $this->db->sqliteCreateFunction('now', 'php_now', 0); + $this->db->sqliteCreateFunction('sqlitedatatype', 'php_sqlite_data_type', 2); + $this->db->sqliteCreateFunction('strleft', 'php_left', 2); + $this->db->sqliteCreateFunction('strright', 'php_right', 2); + } else { + $error_message = "file not found"; + $message['message'] = $error_message; + $this->message = $message; + return false; + } + } + + if ($this->driver == "mysql") { + try { + //mysql pdo connection + if (strlen($this->host) == 0 && empty($this->port)) { + //if both host and port are empty use the unix socket + $this->db = new PDO("mysql:host=$this->host;unix_socket=/var/run/mysqld/mysqld.sock;dbname=$this->db_name", $this->username, $this->password); + } else { + if (empty($this->port)) { + //leave out port if it is empty + $this->db = new PDO("mysql:host=$this->host;dbname=$this->db_name;", $this->username, $this->password, [ + PDO::ATTR_ERRMODE, + PDO::ERRMODE_EXCEPTION, + ]); + } else { + $this->db = new PDO("mysql:host=$this->host;port=$this->port;dbname=$this->db_name;", $this->username, $this->password, [ + PDO::ATTR_ERRMODE, + PDO::ERRMODE_EXCEPTION, + ]); + } + } + } catch (PDOException $e) { + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; + } + } + + if ($this->driver == "pgsql") { + //database connection + try { + if (!empty($this->host)) { + if (empty($this->port)) { + $this->port = "5432"; + } + if ($this->db_secure === true) { + $this->db = new PDO("pgsql:host=$this->host port=$this->port dbname=$this->db_name user=$this->username password=$this->password sslmode=$this->ssl_mode sslrootcert=$this->db_cert_authority"); + } else { + $this->db = new PDO("pgsql:host=$this->host port=$this->port dbname=$this->db_name user=$this->username password=$this->password"); + } + } else { + $this->db = new PDO("pgsql:dbname=$this->db_name user=$this->username password=$this->password"); + } + } catch (PDOException $e) { + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; + } + } + + if ($this->driver == "odbc") { + //database connection + try { + $this->db = new PDO("odbc:" . $this->db_name, $this->username, $this->password); + } catch (PDOException $e) { + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; + } + } + + //connected to the database + return true; + } + + /** + * Returns the depth of an array + * + * @param array $array Reference to array + * + * @return int Depth of array + * @internal Moved to class to conserve resources. + */ + public static function array_depth(array &$array) { + $depth = 0; + if (is_array($array)) { + $depth++; + foreach ($array as $value) { + if (is_array($value)) { + $depth = self::array_depth($value) + 1; + } + } + } + return $depth; + } + + /** + * Searches through all fields to see if domain_uuid exists + * + * @param string $name + * + * @return boolean true on success and false on failure + * @see database::get_apps() + * @uses self::$apps directly + */ + public static function domain_uuid_exists($name) { + //get the $apps array from the installed apps from the core and mod directories + if (count(self::$apps) == 0) { + self::get_apps(); + } + + //search through all fields to see if domain_uuid exists + foreach (self::$apps as $x => &$app) { + if (is_array($app['db'])) { + foreach ($app['db'] as $y => $row) { + if (is_array($row['table']['name'])) { + $table_name = $row['table']['name']['text']; + } else { + $table_name = $row['table']['name']; + } + if ($table_name === self::TABLE_PREFIX . $name) { + if (is_array($row['fields'])) { + foreach ($row['fields'] as $field) { + if ($field['name'] == "domain_uuid") { + return true; + } + } //foreach + } //is array + } + } //foreach + } //is array + } //foreach + + //not found + return false; + } + + /** + * Returns a new connected database object.
    + *

    This allows a shortcut for a common syntax. For more information + * on how the connection happens see {@link database::__construct()} and + * {@link database::connect()}

    + *

    Usage:
    + *   $database_object = database::new();

    + * + * @return database the new instance of the database object already connected + * @see database::__construct() + * @see database::connect() + */ + public static function new(array $params = []) { + + //re-use the database connection + if (self::$database === null) { + self::$database = new database($params); + if (!self::$database->is_connected()) { + self::$database->connect(); + } + } + + //set the user_uuid + if (!empty($params['user_uuid'])) { + //use the parameter as the first priority when available + self::$database->user_uuid = $params['user_uuid']; + } elseif (!empty($_SESSION['user_uuid'])) { + //use the session when available + self::$database->user_uuid = $_SESSION['user_uuid']; + } + + //set the domain_uuid + if (!empty($params['domain_uuid'])) { + //use the parameter as the first priority when available + self::$database->domain_uuid = $params['domain_uuid']; + } elseif (!empty($_SESSION['domain_uuid'])) { + //use the session when available + self::$database->domain_uuid = $_SESSION['domain_uuid']; + } + + return self::$database; + } + + /** + * Ensure the database is still connected and active. + *

    NOTE:
    + * There is no method in PDO that can reliably detect if the connection is active. Therefore, a lightweight + * query is executed using the statement select 1.

    + * + * @return bool True if the database is connected. False otherwise. + */ + public function is_connected(): bool { + try { + $stmt = false; + if ($this->db !== null) $stmt = $this->db->query('SELECT 1'); + return $stmt !== false; + } catch (PDOException $ex) { + //database is not connected + return false; + } catch (Exception $e) { + //some other error has occurred, so record it + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; + } + return true; + } + + /** + * Magic function called whenever a property is requested. + *

    If any case statement is removed then access to the variable will be removed.

    + * + * @param mixed $name object property + * + * @return mixed + */ + public function __get($name) { + //remove any case statement below to remove access to the variable + switch ($name) { + case 'name': + return $this->app_name; + case 'app_name': + case 'app_uuid': + case 'db': + case 'db_cert_authority': + case 'db_name': + case 'db_secure': + case 'domain_uuid': + case 'driver': + case 'fields': + case 'host': + case 'limit': + case 'message': + case 'offset': + case 'order_by': + case 'order_type': + case 'password': + case 'path': + case 'port': + case 'result': + case 'sql': + case 'table': + case 'type': + case 'username': + case 'where': + case 'debug': + return $this->{$name}; + case 'count': + return $this->count(); + default: + trigger_error('Object property not available', E_USER_ERROR); + } + } + /** *

    Magic function called whenever a property is attempted to be set.

    *

    This is used to protect the values stored in the object properties.

    - * @param mixed $name Name of object property + * + * @param mixed $name Name of object property * @param mixed $value Value of property */ - public function __set($name,$value) { - switch($name) { + public function __set($name, $value) { + switch ($name) { case 'name': case 'app_name': $this->app_name = self::sanitize($value); @@ -384,16 +745,22 @@ class database { break; case 'port': $value = (int)$value; // force cast to int - if ($value > 1023 && $value < 65536) { $this->port = $value; } //valid values are 1024...65535 - else { trigger_error('Port not a valid range', E_USER_ERROR); } + if ($value > 1023 && $value < 65536) { + $this->port = $value; + } //valid values are 1024...65535 + else { + trigger_error('Port not a valid range', E_USER_ERROR); + } break; case 'app_uuid': case 'domain_uuid': - if (is_uuid($value)) { $this->domain_uuid = $value; } + if (is_uuid($value)) { + $this->domain_uuid = $value; + } break; case 'type': case 'driver': - switch($value) { + switch ($value) { case 'pgsql': case 'mysql': case 'sqlite': @@ -426,324 +793,222 @@ class database { } /** - * Magic function called whenever a property is requested. - *

    If any case statement is removed then access to the variable will be removed.

    - * @param mixed $name object property - * @return mixed + * Returns a sanitized string value safe for the database or table name. + * + * @param string $value To be sanitized + * + * @return string Sanitized using preg_replace('#[^a-zA-Z0-9_\-]#', '') + * @see preg_replace() */ - public function __get($name) { - //remove any case statement below to remove access to the variable - switch($name) { - case 'name': - return $this->app_name; - case 'app_name': - case 'app_uuid': - case 'db': - case 'db_cert_authority': - case 'db_name': - case 'db_secure': - case 'domain_uuid': - case 'driver': - case 'fields': - case 'host': - case 'limit': - case 'message': - case 'offset': - case 'order_by': - case 'order_type': - case 'password': - case 'path': - case 'port': - case 'result': - case 'sql': - case 'table': - case 'type': - case 'username': - case 'where': - case 'debug': - case 'count': - return $this->count(); - default: - trigger_error('Object property not available', E_USER_ERROR); + public static function sanitize(string $value) { + return preg_replace('#[^a-zA-Z0-9_\-]#', '', $value); + } + +/** + * Counts the number of rows. + * + * @return int Represents the number of counted rows or -1 if failed. + */ + public function count() { + + //connect to the database if needed + if (!$this->db) { + $this->connect(); + } + + //return if the table name is not set + if (empty($this->table)) { + return; + } + + //sanitize the table name + //$this->table = self::sanitize($this->table); // no longer needed + + //get the number of rows + $sql = "select count(*) as num_rows from " . $this->table . " "; + $i = 0; + if (is_array($this->where)) { + foreach ($this->where as $row) { + //sanitize the name + $row['name'] = self::sanitize($row['name']); + + //validate the operator + switch ($row['operator']) { + case "<": + break; + case ">": + break; + case "<=": + break; + case ">=": + break; + case "=": + break; + case "<>": + break; + case "!=": + break; + default: + //invalid operator + return -1; + } + + //build the sql + if ($i == 0) { + $sql .= "where " . $row['name'] . " " . $row['operator'] . " :" . $row['name'] . " "; + } else { + $sql .= "and " . $row['name'] . " " . $row['operator'] . " :" . $row['name'] . " "; + } + + //add the name and value to the params array + $params[$row['name']] = $row['value']; + + //increment $i + $i++; + } + } + + //unset($this->where); //should not be objects resposibility + $prep_statement = $this->db->prepare($sql); + if ($prep_statement) { + if (!isset($params)) { + $params = null; + } + $prep_statement->execute($params); + $row = $prep_statement->fetch(PDO::FETCH_ASSOC); + if ($row['num_rows'] > 0) { + return $row['num_rows']; + } else { + return 0; + } + } + unset($prep_statement); + + } + + public function execute($sql, $parameters = null, $return_type = 'all') { + + //connect to the database if needed + if (!$this->db) { + $this->connect(); + } + + //set the error mode + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + //run the query, and return the results + try { + $prep_statement = $this->db->prepare($sql); + if (is_array($parameters)) { + $prep_statement->execute($parameters); + } else { + $prep_statement->execute(); + } + $message["message"] = "OK"; + $message["code"] = "200"; + $message["sql"] = $sql; + if (is_array($parameters)) { + $message["parameters"] = $parameters; + } + $this->message = $message; + + //return the results + switch ($return_type) { + case 'all': + return $prep_statement->fetchAll(PDO::FETCH_ASSOC); + case 'row': + return $prep_statement->fetch(PDO::FETCH_ASSOC); + case 'column'; + return $prep_statement->fetchColumn(); + default: + return $prep_statement->fetchAll(PDO::FETCH_ASSOC); + } + } catch (PDOException $e) { + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; } } + // Use this function to run complex queries + /** * Returns the config object used to create this database object + * * @return config Config object */ public function config(): config { return $this->config; } - /** - *

    Connect to the database.

    - *

    Database driver must be set before calling connect.

    - *

    For types other than sqlite. Execution will stop on failure.

    - * @depends database::driver Alias of database::type. - * - */ - public function connect() { - - //get the database connection settings - //$db_type = $conf['database.0.type']; - //$db_host = $conf['database.0.host']; - //$db_port = $conf['database.0.port']; - //$db_name = $conf['database.0.name']; - //$db_username = $conf['database.0.username']; - //$db_password = $conf['database.0.password']; - - //debug info - //echo "db type:".$db_type."\n"; - //echo "db host:".$db_host."\n"; - //echo "db port:".$db_port."\n"; - //echo "db name:".$db_name."\n"; - //echo "db username:".$db_username."\n"; - //echo "db password:".$db_password."\n"; - //echo "db path:".$db_path."\n"; - //echo "\n"; - - //set defaults - if (!isset($this->driver) && isset($db_type)) { $this->driver = $db_type; } - if (!isset($this->type) && isset($db_type)) { $this->type = $db_type; } - if (!isset($this->host) && isset($db_host)) { $this->host = $db_host; } - if (!isset($this->port) && isset($db_port)) { $this->port = $db_port; } - if (!isset($this->db_name) && isset($db_name)) { $this->db_name = $db_name; } - if (!isset($this->db_secure) && isset($db_secure)) { - $this->db_secure = $db_secure; - } - else { - $this->db_secure = false; - } - if (!isset($this->username) && isset($db_username)) { $this->username = $db_username; } - if (!isset($this->password) && isset($db_password)) { $this->password = $db_password; } - if (!isset($this->path) && isset($db_path)) { $this->path = $db_path; } - - if ($this->driver == "sqlite") { - if (empty($this->db_name)) { - $server_name = $_SERVER["SERVER_NAME"]; - $server_name = str_replace ("www.", "", $server_name); - $db_name_short = $server_name; - $this->db_name = $server_name.'.db'; - } - else { - $db_name_short = $this->db_name; - } - $this->path = realpath($this->path); - if (file_exists($this->path.'/'.$this->db_name)) { - //connect to the database - $this->db = new PDO('sqlite:'.$this->path.'/'.$this->db_name); //sqlite 3 - //PRAGMA commands - $this->db->query('PRAGMA foreign_keys = ON;'); - $this->db->query('PRAGMA journal_mode = wal;'); - //add additional functions to SQLite so that they are accessible inside SQL - //bool PDO::sqliteCreateFunction ( string function_name, callback callback [, int num_args] ) - $this->db->sqliteCreateFunction('md5', 'php_md5', 1); - $this->db->sqliteCreateFunction('unix_timestamp', 'php_unix_timestamp', 1); - $this->db->sqliteCreateFunction('now', 'php_now', 0); - $this->db->sqliteCreateFunction('sqlitedatatype', 'php_sqlite_data_type', 2); - $this->db->sqliteCreateFunction('strleft', 'php_left', 2); - $this->db->sqliteCreateFunction('strright', 'php_right', 2); - } - else { - $error_message = "file not found"; - $message['message'] = $error_message; - $this->message = $message; - return false; - } - } - - if ($this->driver == "mysql") { - try { - //mysql pdo connection - if (strlen($this->host) == 0 && empty($this->port)) { - //if both host and port are empty use the unix socket - $this->db = new PDO("mysql:host=$this->host;unix_socket=/var/run/mysqld/mysqld.sock;dbname=$this->db_name", $this->username, $this->password); - } - else { - if (empty($this->port)) { - //leave out port if it is empty - $this->db = new PDO("mysql:host=$this->host;dbname=$this->db_name;", $this->username, $this->password, array( - PDO::ATTR_ERRMODE, - PDO::ERRMODE_EXCEPTION - )); - } - else { - $this->db = new PDO("mysql:host=$this->host;port=$this->port;dbname=$this->db_name;", $this->username, $this->password, array( - PDO::ATTR_ERRMODE, - PDO::ERRMODE_EXCEPTION - )); - } - } - } - catch (PDOException $e) { - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; - } - } - - if ($this->driver == "pgsql") { - //database connection - try { - if (!empty($this->host)) { - if (empty($this->port)) { $this->port = "5432"; } - if ($this->db_secure === true) { - $this->db = new PDO("pgsql:host=$this->host port=$this->port dbname=$this->db_name user=$this->username password=$this->password sslmode=$this->ssl_mode sslrootcert=$this->db_cert_authority"); - } - else { - $this->db = new PDO("pgsql:host=$this->host port=$this->port dbname=$this->db_name user=$this->username password=$this->password"); - } - } - else { - $this->db = new PDO("pgsql:dbname=$this->db_name user=$this->username password=$this->password"); - } - } - catch (PDOException $e) { - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; - } - } - - if ($this->driver == "odbc") { - //database connection - try { - $this->db = new PDO("odbc:".$this->db_name, $this->username, $this->password); - } - catch (PDOException $e) { - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; - } - } - - //connected to the database - return true; - } - - /** + /** * Returns the table names from the database. + * * @return array tables * @depends connect() */ public function tables() { - $result = []; + $result = []; //connect to the database if needed - if (!$this->db) { - $this->connect(); - } - if ($this->type == "sqlite") { - $sql = "SELECT name FROM sqlite_master "; - $sql .= "WHERE type='table' "; - $sql .= "order by name;"; - } - if ($this->type == "pgsql") { - $sql = "select table_name as name "; - $sql .= "from information_schema.tables "; - $sql .= "where table_schema='public' "; - $sql .= "and table_type='BASE TABLE' "; - $sql .= "order by table_name "; - } - if ($this->type == "mysql") { - $sql = "show tables"; - } - if ($this->type == "mssql") { - $sql = "SELECT * FROM sys.Tables order by name asc"; - } - $prep_statement = $this->db->prepare($sql); - $prep_statement->execute(); - $tmp = $prep_statement->fetchAll(PDO::FETCH_NAMED); - if ($this->type == "pgsql" || $this->type == "sqlite" || $this->type == "mssql") { - if (is_array($tmp)) { - foreach ($tmp as $row) { - $result[]['name'] = $row['name']; - } + if (!$this->db) { + $this->connect(); + } + if ($this->type == "sqlite") { + $sql = "SELECT name FROM sqlite_master "; + $sql .= "WHERE type='table' "; + $sql .= "order by name;"; + } + if ($this->type == "pgsql") { + $sql = "select table_name as name "; + $sql .= "from information_schema.tables "; + $sql .= "where table_schema='public' "; + $sql .= "and table_type='BASE TABLE' "; + $sql .= "order by table_name "; + } + if ($this->type == "mysql") { + $sql = "show tables"; + } + if ($this->type == "mssql") { + $sql = "SELECT * FROM sys.Tables order by name asc"; + } + $prep_statement = $this->db->prepare($sql); + $prep_statement->execute(); + $tmp = $prep_statement->fetchAll(PDO::FETCH_NAMED); + if ($this->type == "pgsql" || $this->type == "sqlite" || $this->type == "mssql") { + if (is_array($tmp)) { + foreach ($tmp as $row) { + $result[]['name'] = $row['name']; } } - if ($this->type == "mysql") { - if (is_array($tmp)) { - foreach ($tmp as $row) { - $table_array = array_values($row); - $result[]['name'] = $table_array[0]; - } + } + if ($this->type == "mysql") { + if (is_array($tmp)) { + foreach ($tmp as $row) { + $table_array = array_values($row); + $result[]['name'] = $table_array[0]; } } - return $result; - } + } + return $result; + } //delete - /** - * Returns table information from the database. - * @return array table info - * @depends connect() - */ - public function table_info() { - //public $db; - //public $type; - //public $table; - //public $name; - - //connect to the database if needed - if (!$this->db) { - $this->connect(); - } - - //get the table info - if (empty($this->table)) { return false; } - if ($this->type == "sqlite") { - $sql = "PRAGMA table_info(".$this->table.");"; - } - if ($this->type == "pgsql") { - $sql = "SELECT ordinal_position, "; - $sql .= "column_name, "; - $sql .= "data_type, "; - $sql .= "column_default, "; - $sql .= "is_nullable, "; - $sql .= "character_maximum_length, "; - $sql .= "numeric_precision "; - $sql .= "FROM information_schema.columns "; - $sql .= "WHERE table_name = '".$this->table."' "; - $sql .= "and table_catalog = '".$this->db_name."' "; - $sql .= "ORDER BY ordinal_position; "; - } - if ($this->type == "mysql") { - $sql = "DESCRIBE ".$this->table.";"; - } - if ($this->type == "mssql") { - $sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '".$this->table."'"; - } - $prep_statement = $this->db->prepare($sql); - $prep_statement->execute(); - - //set the result array - return $prep_statement->fetchAll(PDO::FETCH_ASSOC); - } - - /** + /** * Checks if the table exists in the database. *

    Note:
    * Table name must be sanitized. Otherwise, a warning will be * emitted and false will be returned.

    + * * @param string $table_name Sanitized name of the table to search for. + * * @return boolean Returns true if the table exists and false if it does not. * @depends connect() */ - public function table_exists (string $table_name) { + public function table_exists(string $table_name) { if (self::sanitize($table_name) != $table_name) { trigger_error('Table Name must be sanitized', E_USER_WARNING); return false; @@ -775,30 +1040,31 @@ class database { $sql .= "select * from pg_tables where schemaname='public' and tablename = '$table_name' "; } if ($this->type == "mysql") { - $sql .= "SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = '".$this->db_name."' and TABLE_NAME = '$table_name' "; + $sql .= "SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = '" . $this->db_name . "' and TABLE_NAME = '$table_name' "; } $prep_statement = $this->db->prepare($sql); $prep_statement->execute(); $result = $prep_statement->fetchAll(PDO::FETCH_NAMED); if (count($result) > 0) { return true; //table exists - } - else { + } else { return false; //table doesn't exist } - } + } //count - /** + /** * Checks if the column exists in the database. *

    Note:
    * Tables and Column names must be sanitized. Otherwise, a warning will be * emitted and false will be returned.

    - * @param string $table_name Sanitized name of the table to search for. + * + * @param string $table_name Sanitized name of the table to search for. * @param string $column_name Sanitized name of the column to search for. + * * @return boolean Returns true if the column exists and false if it does not. * @depends connect() */ - public function column_exists (string $table_name, string $column_name) { + public function column_exists(string $table_name, string $column_name) { //sanitize the table name if (self::sanitize($table_name) != $table_name) { trigger_error('Table Name must be sanitized', E_USER_WARNING); @@ -820,7 +1086,7 @@ class database { if (!$this->db) { $backtrace = debug_backtrace(); echo "Connection Failed
    \n"; - echo "line number ".__line__."
    \n"; + echo "line number " . __line__ . "
    \n"; echo "
    ";
     			print_r($backtrace);
     			echo "
    "; @@ -852,8 +1118,7 @@ class database { //return the results from the sql query if (empty($sql)) { return false; - } - else { + } else { $prep_statement = $this->db->prepare($sql); $prep_statement->execute(); $result = $prep_statement->fetchAll(PDO::FETCH_NAMED); @@ -862,18 +1127,18 @@ class database { } if (count($result) > 0) { return true; - } - else { + } else { return false; } unset ($prep_statement); } - } + } //select /** * Queries {@link database::table_info()} to return the fields. - * @access public + * + * @access public * @return array Two dimensional array * @depends table_info() */ @@ -884,46 +1149,96 @@ class database { //public $name; //initialize the array - $result = []; + $result = []; //get the table info - $table_info = $this->table_info(); + $table_info = $this->table_info(); //set the list of fields - if ($this->type == "sqlite") { - if (is_array($table_info)) { - foreach($table_info as $row) { - $result[]['name'] = $row['name']; - } + if ($this->type == "sqlite") { + if (is_array($table_info)) { + foreach ($table_info as $row) { + $result[]['name'] = $row['name']; } } - if ($this->type == "pgsql") { - if (is_array($table_info)) { - foreach($table_info as $row) { - $result[]['name'] = $row['column_name']; - } + } + if ($this->type == "pgsql") { + if (is_array($table_info)) { + foreach ($table_info as $row) { + $result[]['name'] = $row['column_name']; } } - if ($this->type == "mysql") { - if (is_array($table_info)) { - foreach($table_info as $row) { - $result[]['name'] = $row['Field']; - } + } + if ($this->type == "mysql") { + if (is_array($table_info)) { + foreach ($table_info as $row) { + $result[]['name'] = $row['Field']; } } - if ($this->type == "mssql") { - if (is_array($table_info)) { - foreach($table_info as $row) { - $result[]['name'] = $row['COLUMN_NAME']; - } + } + if ($this->type == "mssql") { + if (is_array($table_info)) { + foreach ($table_info as $row) { + $result[]['name'] = $row['COLUMN_NAME']; } } + } //return the result array - return $result; + return $result; } /** + * Returns table information from the database. + * + * @return array table info + * @depends connect() + */ + public function table_info() { + //public $db; + //public $type; + //public $table; + //public $name; + + //connect to the database if needed + if (!$this->db) { + $this->connect(); + } + + //get the table info + if (empty($this->table)) { + return false; + } + if ($this->type == "sqlite") { + $sql = "PRAGMA table_info(" . $this->table . ");"; + } + if ($this->type == "pgsql") { + $sql = "SELECT ordinal_position, "; + $sql .= "column_name, "; + $sql .= "data_type, "; + $sql .= "column_default, "; + $sql .= "is_nullable, "; + $sql .= "character_maximum_length, "; + $sql .= "numeric_precision "; + $sql .= "FROM information_schema.columns "; + $sql .= "WHERE table_name = '" . $this->table . "' "; + $sql .= "and table_catalog = '" . $this->db_name . "' "; + $sql .= "ORDER BY ordinal_position; "; + } + if ($this->type == "mysql") { + $sql = "DESCRIBE " . $this->table . ";"; + } + if ($this->type == "mssql") { + $sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" . $this->table . "'"; + } + $prep_statement = $this->db->prepare($sql); + $prep_statement->execute(); + + //set the result array + return $prep_statement->fetchAll(PDO::FETCH_ASSOC); + } + + /** * Searches database using the following object properties: *
      *
    1. table - sanitized name of the table {@see database::table}
    2. @@ -932,6 +1247,7 @@ class database { *
    3. limit - limit clause {@see database::limit}
    4. *
    5. offset - offset clause {@see database::offset}
    6. *
    + * * @return boolean * @depends connect() */ @@ -944,565 +1260,47 @@ class database { //offset; //connect to the database if needed - if (!$this->db) { - $this->connect(); - } + if (!$this->db) { + $this->connect(); + } //get data from the database - $sql = "select * from ".$this->table." "; - if ($this->where) { - $i = 0; - if (is_array($this->where)) { - foreach($this->where as $row) { - //sanitize the name - $row['name'] = self::sanitize($row['name']); - - //validate the operator - switch ($row['operator']) { - case "<": break; - case ">": break; - case "<=": break; - case ">=": break; - case "=": break; - case "<>": break; - case "!=": break; - default: - //invalid operator - return false; - } - - //build the sql - if ($i == 0) { - //$sql .= 'where '.$row['name']." ".$row['operator']." '".$row['value']."' "; - $sql .= 'where '.$row['name']." ".$row['operator']." :".$row['name']." "; - } - else { - //$sql .= "and ".$row['name']." ".$row['operator']." '".$row['value']."' "; - $sql .= "and ".$row['name']." ".$row['operator']." :".$row['name']." "; - } - - //add the name and value to the params array - $params[$row['name']] = $row['value']; - - //increment $i - $i++; - } - } - } - if (is_array($this->order_by)) { - $sql .= "order by "; - $i = 1; - if (is_array($this->order_by)) { - foreach($this->order_by as $row) { - //sanitize the name - $row['name'] = self::sanitize($row['name']); - - //sanitize the order - switch ($row['order']) { - case "asc": - break; - case "desc": - break; - default: - $row['order'] = ''; - } - - //build the sql - if (count($this->order_by) == $i) { - $sql .= $row['name']." ".$row['order']." "; - } - else { - $sql .= $row['name']." ".$row['order'].", "; - } - - //increment $i - $i++; - } - } - } - - //limit - if (isset($this->limit) && is_numeric($this->limit)) { - $sql .= "limit ".$this->limit." "; - } - //offset - if (isset($this->offset) && is_numeric($this->offset)) { - $sql .= "offset ".$this->offset." "; - } - - $prep_statement = $this->db->prepare($sql); - if ($prep_statement) { - $prep_statement->execute($params); - $array = $prep_statement->fetchAll(PDO::FETCH_ASSOC); - unset($prep_statement); - return $array; - } - else { - return false; - } - } - - // Use this function to run complex queries - public function execute($sql, $parameters = null, $return_type = 'all') { - - //connect to the database if needed - if (!$this->db) { - $this->connect(); - } - - //set the error mode - $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - //run the query, and return the results - try { - $prep_statement = $this->db->prepare($sql); - if (is_array($parameters)) { - $prep_statement->execute($parameters); - } - else { - $prep_statement->execute(); - } - $message["message"] = "OK"; - $message["code"] = "200"; - $message["sql"] = $sql; - if (is_array($parameters)) { - $message["parameters"] = $parameters; - } - $this->message = $message; - - //return the results - switch($return_type) { - case 'all': - return $prep_statement->fetchAll(PDO::FETCH_ASSOC); - case 'row': - return $prep_statement->fetch(PDO::FETCH_ASSOC); - case 'column'; - return $prep_statement->fetchColumn(); - default: - return $prep_statement->fetchAll(PDO::FETCH_ASSOC); - } - } - catch(PDOException $e) { - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; - } - } - - public function delete(array $array) { - //set the default value - $retval = true; - - //return the array - if (!is_array($array)) { return false; } - - //connect to the database if needed - if (!$this->db) { - $this->connect(); - } - - //set the message id - $m = 0; - - //debug sql - //$this->debug["sql"] = true; - - //set the message id - $m = 0; - - //loop through the array - $checked = false; - $x = 0; - foreach ($array as $parent_name => $tables) { - if (is_array($tables)) { - - //get the application name and uuid - if (class_exists($parent_name) && defined("$parent_name::app_name")) { - $this->app_name = $parent_name::app_name; - $this->app_uuid = $parent_name::app_uuid; - } - - //process the array - foreach ($tables as $id => $row) { - - //prepare the variables - $parent_name = self::sanitize($parent_name); - $parent_key_name = self::singular($parent_name)."_uuid"; - - //build the delete array - if (!empty($row['checked']) && $row['checked'] == 'true') { - //set checked to true - $checked = true; - - //delete the child data - if (isset($row[$parent_key_name])) { - $new_array[$parent_name][$x][$parent_key_name] = $row[$parent_key_name]; - } - - //remove the row from the main array - unset($array[$parent_name][$x]); - } - - //loop through the fields - foreach($row as $field_name => $field_value) { - - //find the child tables - $y = 0; - if (is_array($field_value)) { - //prepare the variables - $child_name = self::sanitize($field_name); - $child_key_name = self::singular($child_name)."_uuid"; - - //loop through the child rows - foreach ($field_value as $sub_row) { - - //build the delete array - if ($row['checked'] == 'true') { - //set checked to true - $checked = true; - - //delete the child data - $new_array[$child_name][][$child_key_name] = $sub_row[$child_key_name]; - - //remove the row from the main array - unset($array[$parent_name][$x][$child_name][$y]); - } - - //increment the value - $y++; - } - } - } - - //increment the value - $x++; - - } - } - } - - //if not checked, then copy the array to the delete array - if (!$checked) { - $new_array = $array; - } - - //get the current data - if (count($new_array) > 0) { - //build an array of tables, fields, and values - foreach($new_array as $table_name => $rows) { - foreach($rows as $row) { - foreach($row as $field_name => $field_value) { - $keys[$table_name][$field_name][] = $field_value; - } - } - } - - //use the array to get a copy of the parent data before deleting it - foreach($new_array as $table_name => $rows) { - foreach($rows as $row) { - $table_name = self::sanitize($table_name); - $sql = "select * from ".self::TABLE_PREFIX.$table_name." "; - $i = 0; - foreach($row as $field_name => $field_value) { - if ($i == 0) { $sql .= "where "; } else { $sql .= "and "; } - $sql .= $field_name." in ( "; - $i = 0; - foreach($keys[$table_name][$field_name] as $field_value) { - $field_name = self::sanitize($field_name); - if ($i > 0) { $sql .= " ,"; } - $sql .= " :".$field_name."_".$i." "; - $i++; - } - $sql .= ") "; - $i = 0; - foreach($keys[$table_name][$field_name] as $field_value) { - $parameters[$field_name.'_'.$i] = $field_value; - $i++; - } - } - } - if (isset($field_value) && $field_value != '') { - $results = $this->execute($sql, $parameters, 'all'); - unset($parameters); - if (is_array($results)) { - $old_array[$table_name] = $results; - } - } - } - - //get relations array - $relations = self::get_relations($parent_name); - - //add child data to the old array - foreach($old_array as $parent_name => $rows) { - //get relations array - $relations = self::get_relations($parent_name); - - //loop through the rows - $x = 0; - foreach($rows as $row) { - if (is_array($relations)) { - foreach ($relations as $relation) { - if ($relation['key']['action']['delete'] == 'cascade') { - //set the child table - $child_table = $relation['table']; - - //remove the v_ prefix - if (substr($child_table, 0, strlen(self::TABLE_PREFIX)) == self::TABLE_PREFIX) { - $child_table = substr($child_table, strlen(self::TABLE_PREFIX)); - } - - //get the child data - $sql = "select * from ".self::TABLE_PREFIX.$child_table." "; - $sql .= "where ".$relation['field']." = :".$relation['field']; - $parameters[$relation['field']] = $row[$relation['field']]; - $results = $this->execute($sql, $parameters, 'all'); - unset($parameters); - if (is_array($results) && $parent_name !== $child_table) { - $old_array[$parent_name][$x][$child_table] = $results; - } - - //delete the child data - if (isset($row[$relation['field']]) && !empty($row[$relation['field']])) { - $sql = "delete from ".self::TABLE_PREFIX.$child_table." "; - $sql .= "where ".$relation['field']." = :".$relation['field']; - $parameters[$relation['field']] = $row[$relation['field']]; -// $this->execute($sql, $parameters); - } - unset($parameters); - } - } - } - $x++; - } - } - } - - //use a try catch around the transaction - try { - - //start the atomic transaction - $this->db->beginTransaction(); - - //delete the current data - foreach($new_array as $table_name => $rows) { - //get the application name and uuid - if (class_exists($table_name) && defined("$table_name::app_name")) { - $this->app_name = $table_name::app_name; - $this->app_uuid = $table_name::app_uuid; - } - if (empty($this->app_name)) { - $app_name_singular = self::singular($table_name); - if (class_exists($app_name_singular) && defined("$app_name_singular::app_name")) { - $this->app_name = $app_name_singular::app_name; - $this->app_uuid = $app_name_singular::app_uuid; - } - } - - //build and run the delete SQL statements - foreach($rows as $row) { - if (permission_exists(self::singular($table_name).'_delete')) { - $sql = "delete from ".self::TABLE_PREFIX.$table_name." "; - $i = 0; - foreach($row as $field_name => $field_value) { - //echo "field: ".$field_name." = ".$field_value."\n"; - if ($i == 0) { $sql .= "where "; } else { $sql .= "and "; } - $sql .= $field_name." = :".$field_name." "; - $parameters[$field_name] = $field_value; - $i++; - } - try { - $this->execute($sql, $parameters); - $message["message"] = "OK"; - $message["code"] = "200"; - $message["uuid"] = $id; - $message["details"][$m]["name"] = $this->app_name; - $message["details"][$m]["message"] = "OK"; - $message["details"][$m]["code"] = "200"; - //$message["details"][$m]["uuid"] = $parent_key_value; - $message["details"][$m]["sql"] = $sql; - - $this->message = $message; - $m++; - unset($sql, $statement); - } - catch(PDOException $e) { - $retval = false; - $message["message"] = "Bad Request"; - $message["code"] = "400"; - $message["details"][$m]["name"] = $this->app_name; - $message["details"][$m]["message"] = $e->getMessage(); - $message["details"][$m]["code"] = "400"; - $message["details"][$m]["sql"] = $sql; - - $this->message = $message; - $m++; - } - unset($parameters); - } //if permission - } //foreach rows - } //foreach $array - - //commit the atomic transaction - $this->db->commit(); - - } catch (\PDOException $e) { - //rollback the transaction on error - if ($this->db->inTransaction()) { - $this->db->rollback(); - } - - //prepare the message array - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; - } - - //set the action if not set - $transaction_type = 'delete'; - - //log the transaction results - if (file_exists($_SERVER["PROJECT_ROOT"]."/app/database_transactions/app_config.php")) { - $sql = "insert into ".self::TABLE_PREFIX."database_transactions "; - $sql .= "("; - $sql .= "database_transaction_uuid, "; - if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { - $sql .= "domain_uuid, "; - } - if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { - $sql .= "user_uuid, "; - } - if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { - $sql .= "app_uuid, "; - } - if (isset($this->app_name) && !empty($this->app_name)) { - $sql .= "app_name, "; - } - $sql .= "transaction_code, "; - $sql .= "transaction_address, "; - $sql .= "transaction_type, "; - $sql .= "transaction_date, "; - $sql .= "transaction_old, "; - $sql .= "transaction_new, "; - $sql .= "transaction_result "; - $sql .= ")"; - $sql .= "values "; - $sql .= "("; - $sql .= "'".uuid()."', "; - if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { - $sql .= "'".$this->domain_uuid."', "; - } - if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { - $sql .= ":user_uuid, "; - } - if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { - $sql .= ":app_uuid, "; - } - if (isset($this->app_name) && !empty($this->app_name)) { - $sql .= ":app_name, "; - } - $sql .= "'".$message["code"]."', "; - $sql .= ":remote_address, "; - $sql .= "'".$transaction_type."', "; - $sql .= "now(), "; - if (is_array($old_array)) { - $sql .= ":transaction_old, "; - } - else { - $sql .= "null, "; - } - if (is_array($new_array)) { - $sql .= ":transaction_new, "; - } - else { - $sql .= "null, "; - } - $sql .= ":transaction_result "; - $sql .= ")"; - $statement = $this->db->prepare($sql); - if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { - $statement->bindParam(':user_uuid', $this->user_uuid); - } - if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { - $statement->bindParam(':app_uuid', $this->app_uuid); - } - if (isset($this->app_name) && !empty($this->app_name)) { - $statement->bindParam(':app_name', $this->app_name); - } - $statement->bindParam(':remote_address', $_SERVER['REMOTE_ADDR']); - if (is_array($old_array)) { - $old_json = json_encode($old_array, JSON_PRETTY_PRINT); - $statement->bindParam(':transaction_old', $old_json); - } - if (is_array($new_array)) { - $new_json = json_encode($new_array, JSON_PRETTY_PRINT); - $statement->bindParam(':transaction_new', $new_json); - } - $result = json_encode($this->message, JSON_PRETTY_PRINT); - $statement->bindParam(':transaction_result', $result); - $statement->execute(); - unset($sql); - } - return $retval; - } //delete - - /** - * Counts the number of rows. - * @return int Represents the number of counted rows or -1 if failed. - */ - public function count() { - - //connect to the database if needed - if (!$this->db) { - $this->connect(); - } - - //return if the table name is not set - if (empty($this->table)) { - return; - } - - //sanitize the table name - //$this->table = self::sanitize($this->table); // no longer needed - - //get the number of rows - $sql = "select count(*) as num_rows from ".$this->table." "; + $sql = "select * from " . $this->table . " "; + if ($this->where) { $i = 0; if (is_array($this->where)) { - foreach($this->where as $row) { + foreach ($this->where as $row) { //sanitize the name $row['name'] = self::sanitize($row['name']); //validate the operator switch ($row['operator']) { - case "<": break; - case ">": break; - case "<=": break; - case ">=": break; - case "=": break; - case "<>": break; - case "!=": break; + case "<": + break; + case ">": + break; + case "<=": + break; + case ">=": + break; + case "=": + break; + case "<>": + break; + case "!=": + break; default: //invalid operator - return -1; + return false; } //build the sql if ($i == 0) { - $sql .= "where ".$row['name']." ".$row['operator']." :".$row['name']." "; - } - else { - $sql .= "and ".$row['name']." ".$row['operator']." :".$row['name']." "; + //$sql .= 'where '.$row['name']." ".$row['operator']." '".$row['value']."' "; + $sql .= 'where ' . $row['name'] . " " . $row['operator'] . " :" . $row['name'] . " "; + } else { + //$sql .= "and ".$row['name']." ".$row['operator']." '".$row['value']."' "; + $sql .= "and " . $row['name'] . " " . $row['operator'] . " :" . $row['name'] . " "; } //add the name and value to the params array @@ -1512,76 +1310,597 @@ class database { $i++; } } + } + if (is_array($this->order_by)) { + $sql .= "order by "; + $i = 1; + if (is_array($this->order_by)) { + foreach ($this->order_by as $row) { + //sanitize the name + $row['name'] = self::sanitize($row['name']); - //unset($this->where); //should not be objects resposibility - $prep_statement = $this->db->prepare($sql); - if ($prep_statement) { - if (!isset($params)) { $params = null; } - $prep_statement->execute($params); - $row = $prep_statement->fetch(PDO::FETCH_ASSOC); - if ($row['num_rows'] > 0) { - return $row['num_rows']; - } - else { - return 0; + //sanitize the order + switch ($row['order']) { + case "asc": + break; + case "desc": + break; + default: + $row['order'] = ''; + } + + //build the sql + if (count($this->order_by) == $i) { + $sql .= $row['name'] . " " . $row['order'] . " "; + } else { + $sql .= $row['name'] . " " . $row['order'] . ", "; + } + + //increment $i + $i++; } } + } + + //limit + if (isset($this->limit) && is_numeric($this->limit)) { + $sql .= "limit " . $this->limit . " "; + } + //offset + if (isset($this->offset) && is_numeric($this->offset)) { + $sql .= "offset " . $this->offset . " "; + } + + $prep_statement = $this->db->prepare($sql); + if ($prep_statement) { + $prep_statement->execute($params); + $array = $prep_statement->fetchAll(PDO::FETCH_ASSOC); unset($prep_statement); + return $array; + } else { + return false; + } + } //end function copy - } //count + public function delete(array $array) { + //set the default value + $retval = true; - /** - * Performs a select query on database using the $sql statement supplied. - * @param string $sql Valid SQL statement. - * @param array|null $parameters Value can be array, empty string, or null. - * @param string $return_type Values can be set to all, row, or column. - * @return mixed Returned values can be array, string, boolean, int, or false. This is dependent on $return_type. - */ + //return the array + if (!is_array($array)) { + return false; + } + + //connect to the database if needed + if (!$this->db) { + $this->connect(); + } + + //set the message id + $m = 0; + + //debug sql + //$this->debug["sql"] = true; + + //set the message id + $m = 0; + + //loop through the array + $checked = false; + $x = 0; + foreach ($array as $parent_name => $tables) { + if (is_array($tables)) { + + //get the application name and uuid + if (class_exists($parent_name) && defined("$parent_name::app_name")) { + $this->app_name = $parent_name::app_name; + $this->app_uuid = $parent_name::app_uuid; + } + + //process the array + foreach ($tables as $id => $row) { + + //prepare the variables + $parent_name = self::sanitize($parent_name); + $parent_key_name = self::singular($parent_name) . "_uuid"; + + //build the delete array + if (!empty($row['checked']) && $row['checked'] == 'true') { + //set checked to true + $checked = true; + + //delete the child data + if (isset($row[$parent_key_name])) { + $new_array[$parent_name][$x][$parent_key_name] = $row[$parent_key_name]; + } + + //remove the row from the main array + unset($array[$parent_name][$x]); + } + + //loop through the fields + foreach ($row as $field_name => $field_value) { + + //find the child tables + $y = 0; + if (is_array($field_value)) { + //prepare the variables + $child_name = self::sanitize($field_name); + $child_key_name = self::singular($child_name) . "_uuid"; + + //loop through the child rows + foreach ($field_value as $sub_row) { + + //build the delete array + if ($row['checked'] == 'true') { + //set checked to true + $checked = true; + + //delete the child data + $new_array[$child_name][][$child_key_name] = $sub_row[$child_key_name]; + + //remove the row from the main array + unset($array[$parent_name][$x][$child_name][$y]); + } + + //increment the value + $y++; + } + } + } + + //increment the value + $x++; + + } + } + } + + //if not checked, then copy the array to the delete array + if (!$checked) { + $new_array = $array; + } + + //get the current data + if (count($new_array) > 0) { + //build an array of tables, fields, and values + foreach ($new_array as $table_name => $rows) { + foreach ($rows as $row) { + foreach ($row as $field_name => $field_value) { + $keys[$table_name][$field_name][] = $field_value; + } + } + } + + //use the array to get a copy of the parent data before deleting it + foreach ($new_array as $table_name => $rows) { + foreach ($rows as $row) { + $table_name = self::sanitize($table_name); + $sql = "select * from " . self::TABLE_PREFIX . $table_name . " "; + $i = 0; + foreach ($row as $field_name => $field_value) { + if ($i == 0) { + $sql .= "where "; + } else { + $sql .= "and "; + } + $sql .= $field_name . " in ( "; + $i = 0; + foreach ($keys[$table_name][$field_name] as $field_value) { + $field_name = self::sanitize($field_name); + if ($i > 0) { + $sql .= " ,"; + } + $sql .= " :" . $field_name . "_" . $i . " "; + $i++; + } + $sql .= ") "; + $i = 0; + foreach ($keys[$table_name][$field_name] as $field_value) { + $parameters[$field_name . '_' . $i] = $field_value; + $i++; + } + } + } + if (isset($field_value) && $field_value != '') { + $results = $this->execute($sql, $parameters, 'all'); + unset($parameters); + if (is_array($results)) { + $old_array[$table_name] = $results; + } + } + } + + //get relations array + $relations = self::get_relations($parent_name); + + //add child data to the old array + foreach ($old_array as $parent_name => $rows) { + //get relations array + $relations = self::get_relations($parent_name); + + //loop through the rows + $x = 0; + foreach ($rows as $row) { + if (is_array($relations)) { + foreach ($relations as $relation) { + if ($relation['key']['action']['delete'] == 'cascade') { + //set the child table + $child_table = $relation['table']; + + //remove the v_ prefix + if (substr($child_table, 0, strlen(self::TABLE_PREFIX)) == self::TABLE_PREFIX) { + $child_table = substr($child_table, strlen(self::TABLE_PREFIX)); + } + + //get the child data + $sql = "select * from " . self::TABLE_PREFIX . $child_table . " "; + $sql .= "where " . $relation['field'] . " = :" . $relation['field']; + $parameters[$relation['field']] = $row[$relation['field']]; + $results = $this->execute($sql, $parameters, 'all'); + unset($parameters); + if (is_array($results) && $parent_name !== $child_table) { + $old_array[$parent_name][$x][$child_table] = $results; + } + + //delete the child data + if (isset($row[$relation['field']]) && !empty($row[$relation['field']])) { + $sql = "delete from " . self::TABLE_PREFIX . $child_table . " "; + $sql .= "where " . $relation['field'] . " = :" . $relation['field']; + $parameters[$relation['field']] = $row[$relation['field']]; +// $this->execute($sql, $parameters); + } + unset($parameters); + } + } + } + $x++; + } + } + } + + //use a try catch around the transaction + try { + + //start the atomic transaction + $this->db->beginTransaction(); + + //delete the current data + foreach ($new_array as $table_name => $rows) { + //get the application name and uuid + if (class_exists($table_name) && defined("$table_name::app_name")) { + $this->app_name = $table_name::app_name; + $this->app_uuid = $table_name::app_uuid; + } + if (empty($this->app_name)) { + $app_name_singular = self::singular($table_name); + if (class_exists($app_name_singular) && defined("$app_name_singular::app_name")) { + $this->app_name = $app_name_singular::app_name; + $this->app_uuid = $app_name_singular::app_uuid; + } + } + + //build and run the delete SQL statements + foreach ($rows as $row) { + if (permission_exists(self::singular($table_name) . '_delete')) { + $sql = "delete from " . self::TABLE_PREFIX . $table_name . " "; + $i = 0; + foreach ($row as $field_name => $field_value) { + //echo "field: ".$field_name." = ".$field_value."\n"; + if ($i == 0) { + $sql .= "where "; + } else { + $sql .= "and "; + } + $sql .= $field_name . " = :" . $field_name . " "; + $parameters[$field_name] = $field_value; + $i++; + } + try { + $this->execute($sql, $parameters); + $message["message"] = "OK"; + $message["code"] = "200"; + $message["uuid"] = $id; + $message["details"][$m]["name"] = $this->app_name; + $message["details"][$m]["message"] = "OK"; + $message["details"][$m]["code"] = "200"; + //$message["details"][$m]["uuid"] = $parent_key_value; + $message["details"][$m]["sql"] = $sql; + + $this->message = $message; + $m++; + unset($sql, $statement); + } catch (PDOException $e) { + $retval = false; + $message["message"] = "Bad Request"; + $message["code"] = "400"; + $message["details"][$m]["name"] = $this->app_name; + $message["details"][$m]["message"] = $e->getMessage(); + $message["details"][$m]["code"] = "400"; + $message["details"][$m]["sql"] = $sql; + + $this->message = $message; + $m++; + } + unset($parameters); + } //if permission + } //foreach rows + } //foreach $array + + //commit the atomic transaction + $this->db->commit(); + + } catch (PDOException $e) { + //rollback the transaction on error + if ($this->db->inTransaction()) { + $this->db->rollback(); + } + + //prepare the message array + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; + } + + //set the action if not set + $transaction_type = 'delete'; + + //log the transaction results + if (file_exists($_SERVER["PROJECT_ROOT"] . "/app/database_transactions/app_config.php")) { + $sql = "insert into " . self::TABLE_PREFIX . "database_transactions "; + $sql .= "("; + $sql .= "database_transaction_uuid, "; + if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { + $sql .= "domain_uuid, "; + } + if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { + $sql .= "user_uuid, "; + } + if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { + $sql .= "app_uuid, "; + } + if (isset($this->app_name) && !empty($this->app_name)) { + $sql .= "app_name, "; + } + $sql .= "transaction_code, "; + $sql .= "transaction_address, "; + $sql .= "transaction_type, "; + $sql .= "transaction_date, "; + $sql .= "transaction_old, "; + $sql .= "transaction_new, "; + $sql .= "transaction_result "; + $sql .= ")"; + $sql .= "values "; + $sql .= "("; + $sql .= "'" . uuid() . "', "; + if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { + $sql .= "'" . $this->domain_uuid . "', "; + } + if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { + $sql .= ":user_uuid, "; + } + if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { + $sql .= ":app_uuid, "; + } + if (isset($this->app_name) && !empty($this->app_name)) { + $sql .= ":app_name, "; + } + $sql .= "'" . $message["code"] . "', "; + $sql .= ":remote_address, "; + $sql .= "'" . $transaction_type . "', "; + $sql .= "now(), "; + if (is_array($old_array)) { + $sql .= ":transaction_old, "; + } else { + $sql .= "null, "; + } + if (is_array($new_array)) { + $sql .= ":transaction_new, "; + } else { + $sql .= "null, "; + } + $sql .= ":transaction_result "; + $sql .= ")"; + $statement = $this->db->prepare($sql); + if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { + $statement->bindParam(':user_uuid', $this->user_uuid); + } + if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { + $statement->bindParam(':app_uuid', $this->app_uuid); + } + if (isset($this->app_name) && !empty($this->app_name)) { + $statement->bindParam(':app_name', $this->app_name); + } + $statement->bindParam(':remote_address', $_SERVER['REMOTE_ADDR']); + if (is_array($old_array)) { + $old_json = json_encode($old_array, JSON_PRETTY_PRINT); + $statement->bindParam(':transaction_old', $old_json); + } + if (is_array($new_array)) { + $new_json = json_encode($new_array, JSON_PRETTY_PRINT); + $statement->bindParam(':transaction_new', $new_json); + } + $result = json_encode($this->message, JSON_PRETTY_PRINT); + $statement->bindParam(':transaction_result', $result); + $statement->execute(); + unset($sql); + } + return $retval; + } //end function toggle + + /** + * Converts a plural English word to singular. + * + * @param string $word English word + * + * @return string Singular version of English word + * @internal Moved to class to conserve resources. + */ + public static function singular(string $word) { + //"-es" is used for words that end in "-x", "-s", "-z", "-sh", "-ch" in which case you add + if (substr($word, -2) == "es") { + if (substr($word, -4) == "sses") { // eg. 'addresses' to 'address' + return substr($word, 0, -2); + } elseif (substr($word, -3) == "ses") { // eg. 'databases' to 'database' (necessary!) + return substr($word, 0, -1); + } elseif (substr($word, -3) == "ies") { // eg. 'countries' to 'country' + return substr($word, 0, -3) . "y"; + } elseif (substr($word, -3, 1) == "x") { + return substr($word, 0, -2); + } elseif (substr($word, -3, 1) == "s") { + return substr($word, 0, -2); + } elseif (substr($word, -3, 1) == "z") { + return substr($word, 0, -2); + } elseif (substr($word, -4, 2) == "sh") { + return substr($word, 0, -2); + } elseif (substr($word, -4, 2) == "ch") { + return substr($word, 0, -2); + } else { + return rtrim($word, "s"); + } + } else { + return rtrim($word, "s"); + } + } //save method + + /** + * Get Relations searches through all fields to find relations + * + * @param string $schema Table name + * + * @return array Returns array or false + * @internal Moved to class to conserve resources. + */ + public static function get_relations($schema) { + + //remove the v_ prefix + if (substr($schema, 0, strlen(self::TABLE_PREFIX)) == self::TABLE_PREFIX) { + $schema = substr($schema, strlen(self::TABLE_PREFIX)); + } + + //sanitize the values + $schema = self::sanitize($schema); + + //get the apps array + $config_list = []; + $directories = ["core", "app"]; + $applications = [$schema, self::singular($schema)]; + foreach ($directories as $directory) { + foreach ($applications as $application) { + $path = $_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/$directory/$application/app_config.php"; + $app_config_files = glob($path); + if ($app_config_files !== false) { + $config_list = array_merge($config_list, $app_config_files); + } + } + } + $x = 0; + foreach ($config_list as $config_path) { + include($config_path); + $x++; + } + + //search through all fields to find relations + if (!empty($apps) && is_array($apps)) { + foreach ($apps as $x => $app) { + foreach ($app['db'] as $y => $row) { + foreach ($row['fields'] as $z => $field) { + if (!empty($field['deprecated']) && $field['deprecated'] != "true") { + if (!empty($field['key']['type']) && $field['key']['type'] == "foreign") { + if ($row['table']['name'] == self::TABLE_PREFIX . $schema || $field['key']['reference']['table'] == self::TABLE_PREFIX . $schema) { + //get the field name + if (!empty($field['name']) && is_array($field['name'])) { + $field_name = trim($field['name']['text']); + } else { + $field_name = trim($field['name']); + } + //build the array + $relations[$i]['table'] = $row['table']['name']; + $relations[$i]['field'] = $field_name; + $relations[$i]['key']['type'] = $field['key']['type']; + $relations[$i]['key']['table'] = $field['key']['reference']['table']; + $relations[$i]['key']['field'] = $field['key']['reference']['field']; + if (isset($field['key']['reference']['action'])) { + $relations[$i]['key']['action'] = $field['key']['reference']['action']; + } + //increment the value + $i++; + } + } + } + unset($field_name); + } + } + } + } + + //return the array + if (!empty($relations) && is_array($relations)) { + return $relations; + } else { + return false; + } + } + +/** + * Performs a select query on database using the $sql statement supplied. + * + * @param string $sql Valid SQL statement. + * @param array|null $parameters Value can be array, empty string, or null. + * @param string $return_type Values can be set to all, row, or column. + * + * @return mixed Returned values can be array, string, boolean, int, or false. This is dependent on + * $return_type. + */ public function select(string $sql, ?array $parameters = [], string $return_type = 'all') { //connect to the database if needed - if (!$this->db) { - $this->connect(); - } + if (!$this->db) { + $this->connect(); + } //unable to connect to the database - if (!$this->db) { - $error_message = "Connection Failed
    \n"; - $error_message .= "line number ".__line__."
    \n"; - $message['message'] = $error_message; - $this->message = $message; - return false; - } + if (!$this->db) { + $error_message = "Connection Failed
    \n"; + $error_message .= "line number " . __line__ . "
    \n"; + $message['message'] = $error_message; + $this->message = $message; + return false; + } //set the error mode - if ($this->db) { - $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - } + if ($this->db) { + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } //reduce prepared statement latency - if ($this->db && defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { - $this->db->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); - } + if ($this->db && defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { + $this->db->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); + } //execute the query and return the results - try { - $prep_statement = $this->db->prepare($sql); - if (is_array($parameters)) { - $prep_statement->execute($parameters); - } - else { - $prep_statement->execute(); - } - $message["message"] = "OK"; - $message["code"] = "200"; - $message["sql"] = $sql; - if (is_array($parameters)) { - $message["parameters"] = $parameters; - } - $this->message = $message; + try { + $prep_statement = $this->db->prepare($sql); + if (is_array($parameters)) { + $prep_statement->execute($parameters); + } else { + $prep_statement->execute(); + } + $message["message"] = "OK"; + $message["code"] = "200"; + $message["sql"] = $sql; + if (is_array($parameters)) { + $message["parameters"] = $parameters; + } + $this->message = $message; - //return the results - switch($return_type) { + //return the results + switch ($return_type) { case 'all': return $prep_statement->fetchAll(PDO::FETCH_ASSOC); case 'row': @@ -1590,23 +1909,24 @@ class database { return $prep_statement->fetchColumn(); default: return $prep_statement->fetchAll(PDO::FETCH_ASSOC); - } } - catch(PDOException $e) { - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; - } - } //select + } catch (PDOException $e) { + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; + } + } - /** + /** * Sets the object $result to sql array + * * @param array $array Array containing the table name, uuid, SQL and where clause. + * * @return database Returns the database object or null. */ public function find_new(array $array) { @@ -1630,7 +1950,7 @@ class database { } //build the query - $sql = "SELECT * FROM ".self::TABLE_PREFIX . $this->name . " "; + $sql = "SELECT * FROM " . self::TABLE_PREFIX . $this->name . " "; if (isset($this->uuid)) { //get the specific uuid $sql .= "WHERE " . self::singular($this->name) . "_uuid = '" . $this->uuid . "' "; @@ -1642,13 +1962,20 @@ class database { if (isset($row['operator'])) { //validate the operator switch ($row['operator']) { - case "<": break; - case ">": break; - case "<=": break; - case ">=": break; - case "=": break; - case "<>": break; - case "!=": break; + case "<": + break; + case ">": + break; + case "<=": + break; + case ">=": + break; + case "=": + break; + case "<>": + break; + case "!=": + break; default: //invalid operator return null; @@ -1714,7 +2041,9 @@ class database { /** * Stores the passed UUID in the object + * * @param string $uuid A valid UUID must be passed + * * @return database Returns this object */ public function uuid(string $uuid) { @@ -1722,513 +2051,1042 @@ class database { return $this; } - /** +/** * Copies records and appends suffix to the column description data - * @param array $array Three dimensional Array. The first dimension is the table name without the prefix 'v_'. Second dimension in the row value as int. Third dimension is the column name. + * + * @param array $array Three dimensional Array. The first dimension is the table name without the prefix 'v_'. + * Second dimension in the row value as int. Third dimension is the column name. + * * @return bool Returns true on success and false on failure. */ public function copy(array $array, $suffix = '(Copy)') { //set default return value - $retval = false; + $retval = false; //return the array - if (!is_array($array)) { return $retval; } + if (!is_array($array)) { + return $retval; + } //initialize array - $copy_array = []; + $copy_array = []; //set the message id - $m = 0; + $m = 0; //loop through the array - $x = 0; - foreach ($array as $parent_name => $tables) { - if (is_array($tables)) { - foreach ($tables as $id => $row) { + $x = 0; + foreach ($array as $parent_name => $tables) { + if (is_array($tables)) { + foreach ($tables as $id => $row) { - //prepare the variables - $parent_name = self::sanitize($parent_name); - $parent_key_name = self::singular($parent_name)."_uuid"; + //prepare the variables + $parent_name = self::sanitize($parent_name); + $parent_key_name = self::singular($parent_name) . "_uuid"; - //build the copy array - if (!empty($row['checked']) && $row['checked'] == 'true') { - //set checked to true - $checked = true; + //build the copy array + if (!empty($row['checked']) && $row['checked'] == 'true') { + //set checked to true + $checked = true; - //copy the child data - if (!empty($row[$parent_key_name]) && is_uuid($row[$parent_key_name])) { - $copy_array[$parent_name][$x][$parent_key_name] = $row[$parent_key_name]; - } - - //remove the row from the main array - unset($array[$parent_name][$x]); - - //loop through the fields - foreach($row as $field_name => $field_value) { - //find the child tables - if (is_array($field_value)) { - - //prepare the variables - $child_name = self::sanitize($field_name); - $child_key_name = self::singular($child_name)."_uuid"; - - //loop through the child rows - $y = 0; - foreach ($field_value as $sub_row) { - - //delete the child data - $copy_array[$child_name][][$child_key_name] = $sub_row[$child_key_name]; - - //remove the row from the main array - unset($array[$parent_name][$x][$child_name][$y]); - - //increment the value - $y++; - } - } - } - } - - //increment the value - $x++; - - } - } - } - - //get the current data - if (count($copy_array) > 0) { - - //build an array of tables, fields, and values - foreach($copy_array as $table_name => $rows) { - foreach($rows as $row) { - foreach($row as $field_name => $field_value) { - $keys[$table_name][$field_name][] = $field_value; + //copy the child data + if (!empty($row[$parent_key_name]) && is_uuid($row[$parent_key_name])) { + $copy_array[$parent_name][$x][$parent_key_name] = $row[$parent_key_name]; } - } - } - //unset the array - unset($array); - - //use the array to get a copy of the paent data before deleting it - foreach($copy_array as $table_name => $rows) { - foreach($rows as $row) { - $table_name = self::sanitize($table_name); - $sql = "select * from ".self::TABLE_PREFIX.$table_name." "; - $i = 0; - foreach($row as $field_name => $field_value) { - if ($i == 0) { $sql .= "where "; } else { $sql .= "and "; } - $sql .= $field_name." in ( "; - $i = 0; - foreach($keys[$table_name][$field_name] as $field_value) { - $field_name = self::sanitize($field_name); - if ($i > 0) { $sql .= " ,"; } - $sql .= " :".$field_name."_".$i." "; - $i++; - } - $sql .= ") "; - $i = 0; - foreach($keys[$table_name][$field_name] as $field_value) { - $parameters[$field_name.'_'.$i] = $field_value; - $i++; - } - } - } - - $results = $this->execute($sql, $parameters, 'all'); - unset($parameters); - if (is_array($results)) { - $array[$table_name] = $results; - } - } - - //add child data to the old array - foreach($copy_array as $parent_name => $rows) { - //get relations array - $relations = self::get_relations($parent_name); - - //loop through the rows - $x = 0; - foreach($rows as $row) { - if (is_array($relations)) { - foreach ($relations as $relation) { - //set the child table - $child_table = $relation['table']; - - //remove the v_ prefix - if (substr($child_table, 0, strlen(self::TABLE_PREFIX)) == self::TABLE_PREFIX) { - $child_table = substr($child_table, strlen(self::TABLE_PREFIX)); - } - - //get the child data - $sql = "select * from ".self::TABLE_PREFIX.$child_table." "; - $sql .= "where ".$relation['field']." = :".$relation['field']; - $parameters[$relation['field']] = $row[$relation['field']]; - $results = $this->execute($sql, $parameters, 'all'); - unset($parameters); - if (is_array($results)) { - $array[$parent_name][$x][$child_table] = $results; - } - } - } - $x++; - } - } - } - - //update the parent and child keys - $checked = false; - $x = 0; - foreach ($array as $parent_name => $tables) { - if (is_array($tables)) { - foreach ($tables as $id => $row) { - - //prepare the variables - $parent_name = self::sanitize($parent_name); - $parent_key_name = self::singular($parent_name)."_uuid"; - $parent_key_value = uuid(); - - //update the parent key id - $array[$parent_name][$x][$parent_key_name] = $parent_key_value; - - //set enabled - if (array_key_exists(self::singular($parent_name).'_enabled', $array[$parent_name][$x])) { - $array[$parent_name][$x][self::singular($parent_name).'_enabled'] = $row[self::singular($parent_name).'_enabled'] === true || $row[self::singular($parent_name).'_enabled'] == 'true' ? 'true' : 'false'; - } - else if (array_key_exists('enabled', $array[$parent_name][$x])) { - $array[$parent_name][$x]['enabled'] = $row['enabled'] === true || $row['enabled'] == 'true' ? 'true' : 'false'; - } - - //add copy to the description - if (array_key_exists(self::singular($parent_name).'_description', $array[$parent_name][$x])) { - $array[$parent_name][$x][self::singular($parent_name).'_description'] = trim($array[$parent_name][$x][self::singular($parent_name).'_description'].' '.$suffix); - } - else if (array_key_exists('description', $array[$parent_name][$x])) { - $array[$parent_name][$x]['description'] = trim($array[$parent_name][$x]['description'].' '.$suffix); - } + //remove the row from the main array + unset($array[$parent_name][$x]); //loop through the fields - foreach($row as $field_name => $field_value) { + foreach ($row as $field_name => $field_value) { + //find the child tables + if (is_array($field_value)) { - //find the child tables + //prepare the variables + $child_name = self::sanitize($field_name); + $child_key_name = self::singular($child_name) . "_uuid"; + + //loop through the child rows $y = 0; - if (is_array($field_value)) { - //prepare the variables - $child_name = self::sanitize($field_name); - $child_key_name = self::singular($child_name)."_uuid"; + foreach ($field_value as $sub_row) { - //loop through the child rows - foreach ($field_value as $sub_row) { - //update the parent key id - $array[$parent_name][$x][$child_name][$y][$parent_key_name] = $parent_key_value; - - //udpate the child key id - $array[$parent_name][$x][$child_name][$y][$child_key_name] = uuid(); - - //increment the value - $y++; - } - } - } - - //increment the value - $x++; - - } - } - } - - //save the copy of the data - if (is_array($array) && count($array) > 0) { - $retval = $this->save($array); - unset($array); - } - return $retval; - } //end function copy - - /** - * Toggles fields on a table using the toggle_field array values within the app object. - * @param array $array Three dimensional Array. The first dimension is the table name without the prefix 'v_'. Second dimension in the row value as int. Third dimension is the column name. - * @return bool Returns true on success and false on failure. - * @depends database::save() - * @depends database::get_apps() - */ - public function toggle(array $array) { - - //return the array - if (!is_array($array)) { return false; } - - //set the message id - $m = 0; - - //loop through the array - if (!empty($array) && is_array($array)) { - $x = 0; - foreach ($array as $parent_name => $tables) { - if (!empty($tables) && is_array($tables)) { - foreach ($tables as $id => $row) { - - //prepare the variables - $parent_name = self::sanitize($parent_name); - $parent_key_name = self::singular($parent_name)."_uuid"; - - //build the toggle array - if (!empty($row['checked']) && $row['checked'] == 'true') { - //toggle the field value - //$toggle_array[$parent_name][$x][$parent_key_name] = $row[$parent_key_name]; - $toggle_array[$parent_name][$x] = $row; + //delete the child data + $copy_array[$child_name][][$child_key_name] = $sub_row[$child_key_name]; //remove the row from the main array - unset($array[$parent_name][$x]); - } + unset($array[$parent_name][$x][$child_name][$y]); - //loop through the fields - foreach($row as $field_name => $field_value) { - - //find the child tables - $y = 0; - if (!empty($field_value) && is_array($field_value)) { - //prepare the variables - $child_name = self::sanitize($field_name); - $child_key_name = self::singular($child_name)."_uuid"; - - //loop through the child rows - foreach ($field_value as $sub_row) { - - //build the delete array - if ($sub_row['checked'] == 'true') { - //delete the child data - $delete_array[$child_name][$y][$child_key_name] = $sub_row[$child_key_name]; - - //remove the row from the main array - unset($array[$parent_name][$x][$child_name][$y]); - } - - //increment the value - $y++; - } - } - } - - //increment the value - $x++; - - } - } - } - } - - //unset the original array - unset($array); - - //get the $apps array from the installed apps from the core and mod directories - if (count(self::$apps) == 0) { - self::get_apps(); - } - - //search through all fields to see if toggle field exists - foreach (self::$apps as $x => $app) { - if (!empty($app['db']) && is_array($app['db'])) { - foreach ($app['db'] as $y => $row) { - if (is_array($row['table']['name'])) { - $table_name = $row['table']['name']['text']; - } - else { - $table_name = $row['table']['name']; - } - if ($table_name === self::TABLE_PREFIX.$parent_name) { - if (is_array($row['fields'])) { - foreach ($row['fields'] as $field) { - if (isset($field['toggle'])) { - $toggle_field = $field['name']; - $toggle_values = $field['toggle']; - } + //increment the value + $y++; } } } } + + //increment the value + $x++; + + } + } + } + + //get the current data + if (count($copy_array) > 0) { + + //build an array of tables, fields, and values + foreach ($copy_array as $table_name => $rows) { + foreach ($rows as $row) { + foreach ($row as $field_name => $field_value) { + $keys[$table_name][$field_name][] = $field_value; + } } } - //if the toggle field and values are empty then set defaults - if (empty($toggle_field)) { - $toggle_field = self::singular($parent_name)."_enabled"; - } - if (empty($toggle_values)) { - $toggle_values[] = 'true'; - $toggle_values[] = 'false'; + //unset the array + unset($array); + + //use the array to get a copy of the parent data before deleting it + foreach ($copy_array as $table_name => $rows) { + foreach ($rows as $row) { + $table_name = self::sanitize($table_name); + $sql = "select * from " . self::TABLE_PREFIX . $table_name . " "; + $i = 0; + foreach ($row as $field_name => $field_value) { + if ($i == 0) { + $sql .= "where "; + } else { + $sql .= "and "; + } + $sql .= $field_name . " in ( "; + $i = 0; + foreach ($keys[$table_name][$field_name] as $field_value) { + $field_name = self::sanitize($field_name); + if ($i > 0) { + $sql .= " ,"; + } + $sql .= " :" . $field_name . "_" . $i . " "; + $i++; + } + $sql .= ") "; + $i = 0; + foreach ($keys[$table_name][$field_name] as $field_value) { + $parameters[$field_name . '_' . $i] = $field_value; + $i++; + } + } + } + + $results = $this->execute($sql, $parameters, 'all'); + unset($parameters); + if (is_array($results)) { + $array[$table_name] = $results; + } } - //get the current values from the database - foreach ($toggle_array as $table_name => $table) { + //add child data to the old array + foreach ($copy_array as $parent_name => $rows) { + //get relations array + $relations = self::get_relations($parent_name); + + //loop through the rows $x = 0; - foreach($table as $row) { - $child_name = self::sanitize($table_name); - $child_key_name = self::singular($child_name)."_uuid"; + foreach ($rows as $row) { + if (is_array($relations)) { + foreach ($relations as $relation) { + //set the child table + $child_table = $relation['table']; - $array[$table_name][$x][$child_key_name] = $row[$child_key_name]; - $array[$table_name][$x][$toggle_field] = ($row[$toggle_field] === $toggle_values[0]) ? $toggle_values[1] : $toggle_values[0]; + //remove the v_ prefix + if (substr($child_table, 0, strlen(self::TABLE_PREFIX)) == self::TABLE_PREFIX) { + $child_table = substr($child_table, strlen(self::TABLE_PREFIX)); + } + + //get the child data + $sql = "select * from " . self::TABLE_PREFIX . $child_table . " "; + $sql .= "where " . $relation['field'] . " = :" . $relation['field']; + $parameters[$relation['field']] = $row[$relation['field']]; + $results = $this->execute($sql, $parameters, 'all'); + unset($parameters); + if (is_array($results)) { + $array[$parent_name][$x][$child_table] = $results; + } + } + } $x++; } } - unset($toggle_array); + } - //save the array - return $this->save($array); + //update the parent and child keys + $checked = false; + $x = 0; + foreach ($array as $parent_name => $tables) { + if (is_array($tables)) { + foreach ($tables as $id => $row) { - } //end function toggle + //prepare the variables + $parent_name = self::sanitize($parent_name); + $parent_key_name = self::singular($parent_name) . "_uuid"; + $parent_key_value = uuid(); - /** + //update the parent key id + $array[$parent_name][$x][$parent_key_name] = $parent_key_value; + + //set enabled + if (array_key_exists(self::singular($parent_name) . '_enabled', $array[$parent_name][$x])) { + $array[$parent_name][$x][self::singular($parent_name) . '_enabled'] = $row[self::singular($parent_name) . '_enabled'] === true || $row[self::singular($parent_name) . '_enabled'] == 'true' ? 'true' : 'false'; + } elseif (array_key_exists('enabled', $array[$parent_name][$x])) { + $array[$parent_name][$x]['enabled'] = $row['enabled'] === true || $row['enabled'] == 'true' ? 'true' : 'false'; + } + + //add copy to the description + if (array_key_exists(self::singular($parent_name) . '_description', $array[$parent_name][$x])) { + $array[$parent_name][$x][self::singular($parent_name) . '_description'] = trim($array[$parent_name][$x][self::singular($parent_name) . '_description'] . ' ' . $suffix); + } elseif (array_key_exists('description', $array[$parent_name][$x])) { + $array[$parent_name][$x]['description'] = trim($array[$parent_name][$x]['description'] . ' ' . $suffix); + } + + //loop through the fields + foreach ($row as $field_name => $field_value) { + + //find the child tables + $y = 0; + if (is_array($field_value)) { + //prepare the variables + $child_name = self::sanitize($field_name); + $child_key_name = self::singular($child_name) . "_uuid"; + + //loop through the child rows + foreach ($field_value as $sub_row) { + //update the parent key id + $array[$parent_name][$x][$child_name][$y][$parent_key_name] = $parent_key_value; + + //udpate the child key id + $array[$parent_name][$x][$child_name][$y][$child_key_name] = uuid(); + + //increment the value + $y++; + } + } + } + + //increment the value + $x++; + + } + } + } + + //save the copy of the data + if (is_array($array) && count($array) > 0) { + $retval = $this->save($array); + unset($array); + } + return $retval; + } + +/** *

    Save an array to the database.

    - *

    Usage Example:

    $row = 0;
    $array['mytable'][$row]['mycolumn'] = "myvalue";
    if ($database->save($array)) {
      echo "Saved Successfully.";
    } else {
      echo "Save Failed.";
    }

    - * @param array $array Three dimensional Array. The first dimension is the table name without the prefix 'v_'. Second dimension in the row value as int. Third dimension is the column name. - * @param bool $transaction_save + *

    Usage Example:

    $row = 0;
    $array['mytable'][$row]['mycolumn'] = "myvalue";
    if + * ($database->save($array)) {
      echo "Saved Successfully.";
    } else {
      echo "Save + * Failed.";
    }

    + * + * + * @param array $array Three dimensional Array. The first dimension is the table name without the prefix 'v_'. + * Second dimension in the row value as int. Third dimension is the column name. + * @param bool $transaction_save + * * @return returns an array with result details */ public function save(array &$array, bool $transaction_save = true) { //prepare the values - $parent_field_names = []; - $child_field_names = []; - $this->message = []; - $parent_key_exists = false; - $parent_key_name = null; - $parent_key_value = null; - $child_key_exists = false; - $child_key_name = null; - $child_key_value = null; - $table_name = null; - $child_table_name = null; + $parent_field_names = []; + $child_field_names = []; + $this->message = []; + $parent_key_exists = false; + $parent_key_name = null; + $parent_key_value = null; + $child_key_exists = false; + $child_key_name = null; + $child_key_value = null; + $table_name = null; + $child_table_name = null; //set default return value - $retval = true; + $retval = true; //return the array - if (!is_array($array)) { return false; } + if (!is_array($array)) { + return false; + } //set the message id - $m = 0; + $m = 0; //debug sql - //$this->debug["sql"] = true; + //$this->debug["sql"] = true; //connect to the database if needed - if (!$this->db) { - $this->connect(); - } + if (!$this->db) { + $this->connect(); + } //use a try catch around the transaction - try { - //start the atomic transaction - $this->db->beginTransaction(); + try { + //start the atomic transaction + $this->db->beginTransaction(); - //loop through the array - if (is_array($array)) foreach ($array as $parent_name => $parent_array) { + //loop through the array + if (is_array($array)) foreach ($array as $parent_name => $parent_array) { - //get the application name and uuid - if (class_exists($parent_name) && defined("$parent_name::app_name")) { - $this->app_name = $parent_name::app_name; - $this->app_uuid = $parent_name::app_uuid; + //get the application name and uuid + if (class_exists($parent_name) && defined("$parent_name::app_name")) { + $this->app_name = $parent_name::app_name; + $this->app_uuid = $parent_name::app_uuid; + } + + //process the parent array, use it to create insert and update SQL statements + if (is_array($parent_array)) foreach ($parent_array as $row_id => $parent_field_array) { + + //set the variables + $parent_name = self::sanitize($parent_name); + $table_name = self::TABLE_PREFIX . $parent_name; + $parent_key_name = self::singular($parent_name) . "_uuid"; + $parent_key_name = self::sanitize($parent_key_name); + + //if the UUID is set, then set parent key exists and value + //determine if the parent_key_exists + $parent_key_exists = false; + if (isset($parent_field_array[$parent_key_name])) { + $parent_key_value = $parent_field_array[$parent_key_name]; + $parent_key_exists = true; + } else { + if (isset($this->uuid)) { + $parent_key_exists = true; + $parent_key_value = $this->uuid; + } else { + $parent_key_value = uuid(); } + } - //process the parent array, use it to create insert and update SQL statements - if (is_array($parent_array)) foreach ($parent_array as $row_id => $parent_field_array) { + //allow characters found in the UUID only. + $parent_key_value = self::sanitize($parent_key_value); - //set the variables - $parent_name = self::sanitize($parent_name); - $table_name = self::TABLE_PREFIX.$parent_name; - $parent_key_name = self::singular($parent_name)."_uuid"; - $parent_key_name = self::sanitize($parent_key_name); + //get the parent field names + $parent_field_names = []; + if (is_array($parent_field_array)) { + foreach ($parent_field_array as $key => $value) { + if (!is_array($value)) { + $parent_field_names[] = self::sanitize($key); + } + } + } - //if the UUID is set, then set parent key exists and value - //determine if the parent_key_exists - $parent_key_exists = false; - if (isset($parent_field_array[$parent_key_name])) { - $parent_key_value = $parent_field_array[$parent_key_name]; - $parent_key_exists = true; - } - else { - if (isset($this->uuid)) { - $parent_key_exists = true; - $parent_key_value = $this->uuid; - } - else { - $parent_key_value = uuid(); + //determine action update or delete, and get the original data + if ($parent_key_exists) { + $sql = "SELECT " . implode(", ", $parent_field_names) . " FROM " . $table_name . " "; + $sql .= "WHERE " . $parent_key_name . " = '" . $parent_key_value . "'; "; + $prep_statement = $this->db->prepare($sql); + if ($prep_statement) { + //get the data + try { + $prep_statement->execute(); + $parent_results = $prep_statement->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + $message["type"] = 'error'; + $message["code"] = $e->getCode(); + $message["message"] = $e->getMessage(); + $message["sql"] = $sql; + $this->message = $message; + return false; + } + + //set the action + if (count($parent_results) > 0) { + $action = "update"; + } else { + $action = "add"; + } + } + unset($prep_statement); + } else { + $action = "add"; + } + + //add a record + if ($action == "add") { + + if (permission_exists(self::singular($parent_name) . '_add')) { + //add to the old and new arrays + $old_array = null; + $new_array[$parent_name][] = $parent_field_array; + + //prepare the insert statement + $params = []; + $sql = "INSERT INTO " . $table_name . " "; + $sql .= "("; + if (!$parent_key_exists) { + $sql .= $parent_key_name . ", "; + } + if (is_array($parent_field_array)) { + foreach ($parent_field_array as $array_key => $array_value) { + if (!is_array($array_value)) { + $array_key = self::sanitize($array_key); + if ($array_key != 'insert_user' && + $array_key != 'insert_date' && + $array_key != 'update_user' && + $array_key != 'update_date') { + $sql .= $array_key . ", "; + } } } + } + $sql .= "insert_date, "; + $sql .= "insert_user "; + $sql .= ") "; + $sql .= "VALUES "; + $sql .= "("; + if (!$parent_key_exists) { + $sql .= ":parent_key_value, "; + $params['parent_key_value'] = $parent_key_value; + } + if (is_array($parent_field_array)) { + foreach ($parent_field_array as $array_key => $array_value) { + if (!is_array($array_value)) { + if ($array_key != 'insert_user' && + $array_key != 'insert_date' && + $array_key != 'update_user' && + $array_key != 'update_date') { + if (!isset($array_value) || $array_value == '') { + $sql .= "null, "; + } elseif ($array_value === "now()") { + $sql .= "now(), "; + } elseif ($array_value === "user_uuid()") { + $sql .= ':' . $array_key . ", "; + $params[$array_key] = $this->user_uuid ?? null; + } elseif ($array_value === "remote_address()") { + $sql .= ':' . $array_key . ", "; + $params[$array_key] = $_SERVER['REMOTE_ADDR']; + } elseif (gettype($array_value) === 'boolean') { + $sql .= ':' . $array_key . ", "; + $params[$array_key] = $array_value; + } else { + $sql .= ':' . $array_key . ", "; + if (gettype($array_value) === 'string') { + $array_value = trim($array_value); + } + $params[$array_key] = $array_value; + } + } + } + } + } + $sql .= "now(), "; + $sql .= ":insert_user "; + $sql .= ");"; - //allow characters found in the UUID only. - $parent_key_value = self::sanitize($parent_key_value); + //add insert user parameter + $params['insert_user'] = $this->user_uuid ?? null; - //get the parent field names - $parent_field_names = array(); + //set the error mode + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + //reduce prepared statement latency + if (defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { + $this->db->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); + } + + //run the query and return the results + try { + $prep_statement = $this->db->prepare($sql); + $prep_statement->execute($params); + unset($prep_statement); + $message["message"] = "OK"; + $message["code"] = "200"; + $message["uuid"] = $parent_key_value; + $message["details"][$m]["name"] = $this->app_name; + $message["details"][$m]["message"] = "OK"; + $message["details"][$m]["code"] = "200"; + $message["details"][$m]["uuid"] = $parent_key_value; + $message["details"][$m]["sql"] = $sql; + if (is_array($params)) { + $message["details"][$m]["params"] = $params; + } + unset($params); + $this->message = $message; + $m++; + } catch (PDOException $e) { + $retval = false; + $message["message"] = "Bad Request"; + $message["code"] = "400"; + $message["details"][$m]["name"] = $this->app_name; + $message["details"][$m]["message"] = $e->getMessage(); + $message["details"][$m]["code"] = "400"; + $message["details"][$m]["array"] = $parent_field_array; + $message["details"][$m]["sql"] = $sql; + if (is_array($params)) { + $message["details"][$m]["params"] = $params; + } + unset($params); + $this->message = $message; + $m++; + } + unset($sql); + } else { + $retval = false; + $message["name"] = $this->app_name; + $message["message"] = "Forbidden, does not have '" . self::singular($parent_name) . "_add'"; + $message["code"] = "403"; + $message["line"] = __line__; + $this->message[] = $message; + $m++; + } + } + + //edit a specific uuid + if ($action == "update") { + if (permission_exists(self::singular($parent_name) . '_edit')) { + + //validate changes + $data_modified = false; + if (is_array($parent_field_array)) { + $i = 0; + foreach ($parent_field_array as $array_key => $array_value) { + //skip child array + if (is_array($array_value)) { + continue; + } + + //get the variable type of the value + $database_field_type = gettype($parent_results[$i][$array_key]); + $user_field_type = gettype($array_value); + + //trim the string and update the value + if ($user_field_type === 'string') { + //trim the string + $array_value = trim($array_value); + + //update the user value + $parent_field_array[$array_key] = $array_value; + } + + //normalize the data to match the database + if ($database_field_type !== $user_field_type) { + //normalize null + if ($array_value === '') { + $array_value = null; + } + + //normalize string + if ($database_field_type === 'string') { + $array_value = (string)$array_value; + } + + //normalize numeric + if ($database_field_type === 'numeric') { + $array_value = intval($array_value); + } + + //normalize boolean + if ($database_field_type === 'boolean') { + if ($array_value === 'true') { + $array_value = true; + } else { + $array_value = false; + } + } + } + + //verify if the data in the database has been modified + if ($parent_results[$i][$array_key] !== $array_value) { + //not matched + //echo "$parent_name.$array_key ".($parent_results[$i][$array_key])." != ".$array_value."\n\n"; + $data_modified = true; + break; + } + + //increment the id + $i; + } + } + + //parent data - process the modified data + if ($data_modified) { + + //remove the child array and update the special values if (is_array($parent_field_array)) { - foreach ($parent_field_array as $key => $value) { - if (!is_array($value)) { - $parent_field_names[] = self::sanitize($key); + foreach ($parent_field_array as $array_key => $array_value) { + if (is_array($array_value)) { + continue; + } + $array_key = self::sanitize($array_key); + if (!isset($array_value) || (isset($array_value) && $array_value === '')) { + $temp_array[$array_key] = null; + } elseif ($array_value === "now()") { + $temp_array[$array_key] = $array_value; + } elseif ($array_value === "user_uuid()") { + $temp_array[$array_key] = $this->user_uuid ?? null; + } elseif ($array_value === "remote_address()") { + $temp_array[$array_key] = $_SERVER['REMOTE_ADDR']; + } else { + if (gettype($array_value) === 'string') { + $array_value = trim($array_value); + } + $temp_array[$array_key] = $array_value; } } } - //determine action update or delete, and get the original data - if ($parent_key_exists) { - $sql = "SELECT ".implode(", ", $parent_field_names)." FROM ".$table_name." "; - $sql .= "WHERE ".$parent_key_name." = '".$parent_key_value."'; "; - $prep_statement = $this->db->prepare($sql); - if ($prep_statement) { - //get the data - try { - $prep_statement->execute(); - $parent_results = $prep_statement->fetchAll(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $message["type"] = 'error'; - $message["code"] = $e->getCode(); - $message["message"] = $e->getMessage(); - $message["sql"] = $sql; - $this->message = $message; - return false; - } + //add to the old and new arrays + $old_array[$parent_name] = $parent_results; + $new_array[$parent_name][] = $temp_array; - //set the action - if (count($parent_results) > 0) { - $action = "update"; - } - else { - $action = "add"; + //empty the temp array + unset($temp_array); + + //prepare the update statement + $params = []; + $sql = "UPDATE " . $table_name . " SET "; + if (is_array($parent_field_array)) { + foreach ($parent_field_array as $array_key => $array_value) { + if (is_array($array_value)) { + continue; + } + if ($array_key != $parent_key_name) { + $array_key = self::sanitize($array_key); + if (!isset($array_value) || (isset($array_value) && $array_value === '')) { + $sql .= $array_key . " = null, "; + } elseif ($array_value === "now()") { + $sql .= $array_key . " = now(), "; + } elseif ($array_value === "user_uuid()") { + $sql .= $array_key . " = :" . $array_key . ", "; + $params[$array_key] = $this->user_uuid ?? null; + } elseif ($array_value === "remote_address()") { + $sql .= $array_key . " = :" . $array_key . ", "; + $params[$array_key] = $_SERVER['REMOTE_ADDR']; + } elseif (gettype($array_value) === 'boolean') { + $sql .= $array_key . " = :" . $array_key . ", "; + $params[$array_key] = $array_value; + } else { + $sql .= $array_key . " = :" . $array_key . ", "; + if (gettype($array_value) === 'string') { + $array_value = trim($array_value); + } + $params[$array_key] = $array_value; } + } } - unset($prep_statement); - } - else { - $action = "add"; } - //add a record - if ($action == "add") { + //add the modified date and user + $sql .= "update_date = now(), "; + $sql .= "update_user = :update_user "; + $params['update_user'] = $this->user_uuid ?? null; + + //add the where with the parent name and value + $sql .= "WHERE " . $parent_key_name . " = :parent_key_value; "; + $params['parent_key_value'] = $parent_key_value; + $sql = str_replace(", WHERE", " WHERE", $sql); + + //add update user parameter + $params['update_user'] = $this->user_uuid ?? null; + + //set the error mode + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + //reduce prepared statement latency + if (defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { + $this->db->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); + } + + //run the query and return the results + try { + $prep_statement = $this->db->prepare($sql); + $prep_statement->execute($params); + $message["message"] = "OK"; + $message["code"] = "200"; + $message["uuid"] = $parent_key_value; + $message["details"][$m]["name"] = $this->app_name; + $message["details"][$m]["message"] = "OK"; + $message["details"][$m]["code"] = "200"; + $message["details"][$m]["uuid"] = $parent_key_value; + $message["details"][$m]["sql"] = $sql; + if (is_array($params)) { + $message["details"][$m]["params"] = $params; + } + unset($params); + $this->message = $message; + $m++; + unset($sql); + } catch (PDOException $e) { + $retval = false; + $message["message"] = "Bad Request"; + $message["code"] = "400"; + $message["details"][$m]["name"] = $this->app_name; + $message["details"][$m]["message"] = $e->getMessage(); + $message["details"][$m]["code"] = "400"; + $message["details"][$m]["sql"] = $sql; + if (is_array($params)) { + $message["details"][$m]["params"] = $params; + } + unset($params); + $this->message = $message; + $m++; + } + } else { + $message["details"][$m]["name"] = $parent_name; + $message["details"][$m]["message"] = 'No Changes'; + $message["details"][$m]["code"] = "000"; + $message["details"][$m]["uuid"] = $parent_key_value; + $this->message = $message; + $m++; + } + } else { + $retval = false; + $message["message"] = "Forbidden, does not have '" . self::singular($parent_name) . "_edit'"; + $message["code"] = "403"; + $message["line"] = __line__; + $this->message = $message; + $m++; + } + } + + //unset the variables + unset($sql, $action); + + //child data + if (is_array($parent_field_array)) { + foreach ($parent_field_array as $key => $value) { + if (is_array($value)) { + $child_table_name = self::TABLE_PREFIX . $key; + $child_table_name = self::sanitize($child_table_name); + foreach ($value as $id => $row) { + //prepare the variables + $child_name = self::singular($key); + $child_name = self::sanitize($child_name); + $child_key_name = $child_name . "_uuid"; + + //determine if the parent key exists in the child array + $parent_key_exists = false; + if (!isset($parent_field_array[$parent_key_name])) { + $parent_key_exists = true; + } + + //determine if the uuid exists + $uuid_exists = false; + if (is_array($row)) foreach ($row as $k => $v) { + if ($child_key_name == $k) { + if (strlen($v) > 0) { + if (gettype($v) === 'string') { + $v = trim($v); + } + $child_key_value = $v; + $uuid_exists = true; + break; + } + } else { + $uuid_exists = false; + } + } + + //allow characters found in the uuid only + if (isset($child_key_value)) { + $child_key_value = self::sanitize($child_key_value); + } + + //get the child field names + $child_field_names = []; + if (is_array($row)) { + foreach ($row as $k => $v) { + if (!is_array($v) && $k !== 'checked') { + $child_field_names[] = self::sanitize($k); + } + } + } + + //determine sql update or delete and get the original data + if ($uuid_exists) { + $sql = "SELECT " . implode(", ", $child_field_names) . " FROM " . $child_table_name . " "; + $sql .= "WHERE " . $child_key_name . " = '" . $child_key_value . "'; "; + try { + $prep_statement = $this->db->prepare($sql); + if ($prep_statement) { + //get the data + $prep_statement->execute(); + $child_results = $prep_statement->fetch(PDO::FETCH_ASSOC); + + //set the action + if (is_array($child_results)) { + $action = "update"; + } else { + $action = "add"; + } + } + unset($prep_statement); + } catch (PDOException $e) { + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; + } + + } else { + $action = "add"; + } + + //update the child data + if ($action == "update") { + if (permission_exists($child_name . '_edit')) { + + //validate changes + $data_modified = false; + if (is_array($row)) { + foreach ($row as $k => $v) { + //sanitize the key + $k = self::sanitize($k); + + //get the variable type of the value + $database_field_type = gettype($child_results[$k]); + $user_field_type = gettype($v); + + //trim the string + if ($user_field_type === 'string') { + $v = trim($v); + } + + //normalize the data to match the database + if ($database_field_type !== $user_field_type) { + //normalize null + if ($v === '') { + $v = null; + } + + //normalize string + if ($database_field_type === 'string') { + $v = (string)$v; + } + + //normalize numeric + if ($database_field_type === 'numeric') { + $v = intval($v); + } + + //normalize boolean + if ($database_field_type === 'boolean') { + if ($v === 'true') { + $v = true; + } else { + $v = false; + } + } + } + + //verify if the data in the database has been modified + if ($child_results[$k] !== $v) { + //not matched + //echo "$child_name.$k ".($child_results[$k])." != ".$v."\n\n"; + $data_modified = true; + break; + } + } + } + + //child data - process the modified data + if ($data_modified) { + + //update the special values + if (is_array($row)) { + foreach ($row as $k => $v) { + //sanitize the key + $k = self::sanitize($k); + + //save the key value pairs to the temp_array + if (!isset($v) || (isset($v) && $v == '')) { + $temp_array[$k] = null; + } elseif ($v === "now()") { + $temp_array[$k] = 'now()'; + } elseif ($v === "user_uuid()") { + $temp_array[$k] = $this->user_uuid ?? null; + } elseif ($v === "remote_address()") { + $temp_array[$k] = $_SERVER['REMOTE_ADDR']; + } + if (gettype($v) === 'boolean') { + if ($v) { + $v = true; + } else { + $v = false; + } + $temp_array[$k] = $v; + } else { + if (gettype($v) === 'string') { + $v = trim($v); + } + $temp_array[$k] = $v; + } + } + } + + //add to the old and new arrays + $old_array[$key][] = $child_results; + $new_array[$key][] = $temp_array; + + //empty the temp array + unset($temp_array); + + //update the child data + $sql = "UPDATE " . $child_table_name . " SET "; + if (is_array($row)) { + foreach ($row as $k => $v) { + if (!is_array($v) && ($k != $parent_key_name || $k != $child_key_name)) { + $k = self::sanitize($k); + if (!isset($v) || (isset($v) && $v == '')) { + $sql .= $k . " = null, "; + } elseif ($v === "now()") { + $sql .= $k . " = now(), "; + } elseif ($v === "user_uuid()") { + $sql .= $k . " = :" . $k . ", "; + $params[$k] = $this->user_uuid ?? null; + } elseif ($v === "remote_address()") { + $sql .= $k . " = :" . $k . ", "; + $params[$k] = $_SERVER['REMOTE_ADDR']; + } elseif (gettype($v) === 'boolean') { + $sql .= $k . " = :" . $k . ", "; + $params[$k] = $v; + } else { + $sql .= $k . " = :" . $k . ", "; + if (gettype($v) === 'string') { + $v = trim($v); + } + $params[$k] = $v; + } + } + } + } + + //add the modified date and user + $sql .= "update_date = now(), "; + $sql .= "update_user = :update_user "; + $params['update_user'] = $this->user_uuid ?? null; + + //add the where with the parent name and value + $sql .= "WHERE " . $parent_key_name . " = :parent_key_value "; + $sql .= "AND " . $child_key_name . " = :child_key_value; "; + $params['parent_key_value'] = $parent_key_value; + $params['child_key_value'] = $child_key_value; + $sql = str_replace(", WHERE", " WHERE", $sql); + + //set the error mode + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + //reduce prepared statement latency + if (defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { + $this->db->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); + } + + try { + $prep_statement = $this->db->prepare($sql); + $prep_statement->execute($params); + unset($prep_statement); + $message["details"][$m]["name"] = $key; + $message["details"][$m]["message"] = "OK"; + $message["details"][$m]["code"] = "200"; + $message["details"][$m]["uuid"] = $child_key_value; + $message["details"][$m]["sql"] = $sql; + if (is_array($params)) { + $message["details"][$m]["params"] = $params; + } + unset($params); + $this->message = $message; + $m++; + } catch (PDOException $e) { + $retval = false; + if ($message["code"] == "200") { + $message["message"] = "Bad Request"; + $message["code"] = "400"; + } + $message["details"][$m]["name"] = $key; + $message["details"][$m]["message"] = $e->getMessage(); + $message["details"][$m]["code"] = "400"; + $message["details"][$m]["sql"] = $sql; + if (is_array($params)) { + $message["details"][$m]["params"] = $params; + } + unset($params); + $this->message = $message; + $m++; + } + + } else { + $message["details"][$m]["name"] = $key; + $message["details"][$m]["message"] = 'No Changes'; + $message["details"][$m]["code"] = "000"; + $message["details"][$m]["uuid"] = $child_key_value; + $this->message = $message; + $m++; + } + } else { + $retval = false; + $message["name"] = $child_name; + $message["message"] = "Forbidden, does not have '" . $child_name . "_edit'"; + $message["code"] = "403"; + $message["line"] = __line__; + $this->message = $message; + $m++; + } + } //action update + + //add the child data + if ($action == "add") { + if (permission_exists($child_name . '_add')) { + //determine if child or parent key exists + $child_key_name = $child_name . '_uuid'; + $parent_key_exists = false; + $child_key_exists = false; + if (is_array($row)) { + foreach ($row as $k => $v) { + if ($k == $parent_key_name) { + $parent_key_exists = true; + } + if ($k == $child_key_name) { + $child_key_exists = true; + if (gettype($v) === 'string') { + $v = trim($v); + } + $child_key_value = $v; + } + } + } + if (!isset($child_key_value) || $child_key_value == '') { + $child_key_value = uuid(); + } - if (permission_exists(self::singular($parent_name).'_add')) { //add to the old and new arrays $old_array = null; - $new_array[$parent_name][] = $parent_field_array; + $new_array[$child_name][] = $row; - //prepare the insert statement - $params = array(); - $sql = "INSERT INTO ".$table_name." "; + //build the insert + $sql = "INSERT INTO " . $child_table_name . " "; $sql .= "("; if (!$parent_key_exists) { - $sql .= $parent_key_name.", "; + $sql .= self::singular($parent_key_name) . ", "; } - if (is_array($parent_field_array)) { - foreach ($parent_field_array as $array_key => $array_value) { - if (!is_array($array_value)) { - $array_key = self::sanitize($array_key); - if ($array_key != 'insert_user' && - $array_key != 'insert_date' && - $array_key != 'update_user' && - $array_key != 'update_date') { - $sql .= $array_key.", "; + if (!$child_key_exists) { + $sql .= self::singular($child_key_name) . ", "; + } + if (is_array($row)) { + foreach ($row as $k => $v) { + if (!is_array($v)) { + $k = self::sanitize($k); + if ($k != 'insert_user' && + $k != 'insert_date' && + $k != 'update_user' && + $k != 'update_date') { + $sql .= $k . ", "; } } } @@ -2242,37 +3100,42 @@ class database { $sql .= ":parent_key_value, "; $params['parent_key_value'] = $parent_key_value; } - if (is_array($parent_field_array)) { - foreach ($parent_field_array as $array_key => $array_value) { - if (!is_array($array_value)) { - if ($array_key != 'insert_user' && - $array_key != 'insert_date' && - $array_key != 'update_user' && - $array_key != 'update_date') { - if (!isset($array_value) || $array_value == '') { + if (!$child_key_exists) { + $sql .= ":child_key_value, "; + $params['child_key_value'] = $child_key_value; + } + if (is_array($row)) { + foreach ($row as $k => $v) { + if (!is_array($v)) { + if ($k != 'insert_user' && + $k != 'insert_date' && + $k != 'update_user' && + $k != 'update_date') { + if (!isset($v) || strlen($v) == 0) { $sql .= "null, "; - } - elseif ($array_value === "now()") { + } elseif ($v === "now()") { $sql .= "now(), "; - } - elseif ($array_value === "user_uuid()") { - $sql .= ':'.$array_key.", "; - $params[$array_key] = $this->user_uuid ?? null; - } - elseif ($array_value === "remote_address()") { - $sql .= ':'.$array_key.", "; - $params[$array_key] = $_SERVER['REMOTE_ADDR']; - } - elseif (gettype($array_value) === 'boolean') { - $sql .= ':'.$array_key.", "; - $params[$array_key] = $array_value; - } - else { - $sql .= ':'.$array_key.", "; - if (gettype($array_value) === 'string') { - $array_value = trim($array_value); + } elseif ($v === "user_uuid()") { + $sql .= ':' . $k . ", "; + $params[$k] = $this->user_uuid ?? null; + } elseif ($v === "remote_address()") { + $sql .= ':' . $k . ", "; + $params[$k] = $_SERVER['REMOTE_ADDR']; + } elseif (gettype($v) === 'boolean') { + $sql .= ':' . $k . ", "; + $params[$k] = $v; + } else { + $k = self::sanitize($k); + if ($k != 'insert_user' && + $k != 'insert_date' && + $k != 'update_user' && + $k != 'update_date') { + $sql .= ':' . $k . ", "; + if (gettype($v) === 'string') { + $v = trim($v); + } + $params[$k] = $v; } - $params[$array_key] = $array_value; } } } @@ -2298,13 +3161,11 @@ class database { $prep_statement = $this->db->prepare($sql); $prep_statement->execute($params); unset($prep_statement); - $message["message"] = "OK"; $message["code"] = "200"; - $message["uuid"] = $parent_key_value; - $message["details"][$m]["name"] = $this->app_name; + $message["details"][$m]["name"] = $key; $message["details"][$m]["message"] = "OK"; $message["details"][$m]["code"] = "200"; - $message["details"][$m]["uuid"] = $parent_key_value; + $message["details"][$m]["uuid"] = $child_key_value; $message["details"][$m]["sql"] = $sql; if (is_array($params)) { $message["details"][$m]["params"] = $params; @@ -2312,220 +3173,13 @@ class database { unset($params); $this->message = $message; $m++; - } - catch(PDOException $e) { + } catch (PDOException $e) { $retval = false; - $message["message"] = "Bad Request"; - $message["code"] = "400"; - $message["details"][$m]["name"] = $this->app_name; - $message["details"][$m]["message"] = $e->getMessage(); - $message["details"][$m]["code"] = "400"; - $message["details"][$m]["array"] = $parent_field_array; - $message["details"][$m]["sql"] = $sql; - if (is_array($params)) { - $message["details"][$m]["params"] = $params; + if ($message["code"] == "200") { + $message["message"] = "Bad Request"; + $message["code"] = "400"; } - unset($params); - $this->message = $message; - $m++; - } - unset($sql); - } - else { - $retval = false; - $message["name"] = $this->app_name; - $message["message"] = "Forbidden, does not have '".self::singular($parent_name)."_add'"; - $message["code"] = "403"; - $message["line"] = __line__; - $this->message[] = $message; - $m++; - } - } - - //edit a specific uuid - if ($action == "update") { - if (permission_exists(self::singular($parent_name).'_edit')) { - - //validate changes - $data_modified = false; - if (is_array($parent_field_array)) { - $i = 0; - foreach ($parent_field_array as $array_key => $array_value) { - //skip child array - if (is_array($array_value)) { continue; } - - //get the variable type of the value - $database_field_type = gettype($parent_results[$i][$array_key]); - $user_field_type = gettype($array_value); - - //trim the string and update the value - if ($user_field_type === 'string') { - //trim the string - $array_value = trim($array_value); - - //update the user value - $parent_field_array[$array_key] = $array_value; - } - - //normalize the data to match the database - if ($database_field_type !== $user_field_type) { - //normalize null - if ($array_value === '') { - $array_value = null; - } - - //normalize string - if ($database_field_type === 'string') { - $array_value = (string)$array_value; - } - - //normalize numeric - if ($database_field_type === 'numeric') { - $array_value = intval($array_value); - } - - //normalize boolean - if ($database_field_type === 'boolean') { - if ($array_value === 'true') { - $array_value = true; - } else { - $array_value = false; - } - } - } - - //verify if the data in the database has been modified - if ($parent_results[$i][$array_key] !== $array_value) { - //not matched - //echo "$parent_name.$array_key ".($parent_results[$i][$array_key])." != ".$array_value."\n\n"; - $data_modified = true; - break; - } - - //increment the id - $i; - } - } - - //parent data - process the modified data - if ($data_modified) { - - //remove the child array and update the special values - if (is_array($parent_field_array)) { - foreach ($parent_field_array as $array_key => $array_value) { - if (is_array($array_value)) { continue; } - $array_key = self::sanitize($array_key); - if (!isset($array_value) || (isset($array_value) && $array_value === '')) { - $temp_array[$array_key] = null; - } - elseif ($array_value === "now()") { - $temp_array[$array_key] = $array_value; - } - elseif ($array_value === "user_uuid()") { - $temp_array[$array_key] = $this->user_uuid ?? null; - } - elseif ($array_value === "remote_address()") { - $temp_array[$array_key] = $_SERVER['REMOTE_ADDR']; - } - else { - if (gettype($array_value) === 'string') { - $array_value = trim($array_value); - } - $temp_array[$array_key] = $array_value; - } - } - } - - //add to the old and new arrays - $old_array[$parent_name] = $parent_results; - $new_array[$parent_name][] = $temp_array; - - //empty the temp array - unset($temp_array); - - //prepare the update statement - $params = array(); - $sql = "UPDATE ".$table_name." SET "; - if (is_array($parent_field_array)) { - foreach ($parent_field_array as $array_key => $array_value) { - if (is_array($array_value)) { continue; } - if ($array_key != $parent_key_name) { - $array_key = self::sanitize($array_key); - if (!isset($array_value) || (isset($array_value) && $array_value === '')) { - $sql .= $array_key." = null, "; - } - elseif ($array_value === "now()") { - $sql .= $array_key." = now(), "; - } - elseif ($array_value === "user_uuid()") { - $sql .= $array_key." = :".$array_key.", "; - $params[$array_key] = $this->user_uuid ?? null; - } - elseif ($array_value === "remote_address()") { - $sql .= $array_key." = :".$array_key.", "; - $params[$array_key] = $_SERVER['REMOTE_ADDR']; - } - elseif (gettype($array_value) === 'boolean') { - $sql .= $array_key." = :".$array_key.", "; - $params[$array_key] = $array_value; - } - else { - $sql .= $array_key." = :".$array_key.", "; - if (gettype($array_value) === 'string') { - $array_value = trim($array_value); - } - $params[$array_key] = $array_value; - } - } - } - } - - //add the modified date and user - $sql .= "update_date = now(), "; - $sql .= "update_user = :update_user "; - $params['update_user'] = $this->user_uuid ?? null; - - //add the where with the parent name and value - $sql .= "WHERE ".$parent_key_name." = :parent_key_value; "; - $params['parent_key_value'] = $parent_key_value; - $sql = str_replace(", WHERE", " WHERE", $sql); - - //add update user parameter - $params['update_user'] = $this->user_uuid ?? null; - - //set the error mode - $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - //reduce prepared statement latency - if (defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { - $this->db->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); - } - - //run the query and return the results - try { - $prep_statement = $this->db->prepare($sql); - $prep_statement->execute($params); - $message["message"] = "OK"; - $message["code"] = "200"; - $message["uuid"] = $parent_key_value; - $message["details"][$m]["name"] = $this->app_name; - $message["details"][$m]["message"] = "OK"; - $message["details"][$m]["code"] = "200"; - $message["details"][$m]["uuid"] = $parent_key_value; - $message["details"][$m]["sql"] = $sql; - if (is_array($params)) { - $message["details"][$m]["params"] = $params; - } - unset($params); - $this->message = $message; - $m++; - unset($sql); - } - catch(PDOException $e) { - $retval = false; - $message["message"] = "Bad Request"; - $message["code"] = "400"; - $message["details"][$m]["name"] = $this->app_name; + $message["details"][$m]["name"] = $key; $message["details"][$m]["message"] = $e->getMessage(); $message["details"][$m]["code"] = "400"; $message["details"][$m]["sql"] = $sql; @@ -2536,692 +3190,41 @@ class database { $this->message = $message; $m++; } - } - else { - $message["details"][$m]["name"] = $parent_name; - $message["details"][$m]["message"] = 'No Changes'; - $message["details"][$m]["code"] = "000"; - $message["details"][$m]["uuid"] = $parent_key_value; + } else { + $retval = false; + $message["name"] = $child_name; + $message["message"] = "Forbidden, does not have '" . $child_name . "_add'"; + $message["code"] = "403"; + $message["line"] = __line__; $this->message = $message; $m++; } - } - else { - $retval = false; - $message["message"] = "Forbidden, does not have '".self::singular($parent_name)."_edit'"; - $message["code"] = "403"; - $message["line"] = __line__; - $this->message = $message; - $m++; - } - } + } //action add - //unset the variables - unset($sql, $action); + //unset the variables + unset($sql, $action, $child_key_name, $child_key_value); + } // foreach value - //child data - if (is_array($parent_field_array)) { - foreach ($parent_field_array as $key => $value) { - if (is_array($value)) { - $child_table_name = self::TABLE_PREFIX.$key; - $child_table_name = self::sanitize($child_table_name); - foreach ($value as $id => $row) { - //prepare the variables - $child_name = self::singular($key); - $child_name = self::sanitize($child_name); - $child_key_name = $child_name."_uuid"; + } //is array + } //foreach array + } - //determine if the parent key exists in the child array - $parent_key_exists = false; - if (!isset($parent_field_array[$parent_key_name])) { - $parent_key_exists = true; - } + } // foreach schema_array + } // foreach main array - //determine if the uuid exists - $uuid_exists = false; - if (is_array($row)) foreach ($row as $k => $v) { - if ($child_key_name == $k) { - if (strlen($v) > 0) { - if (gettype($v) === 'string') { - $v = trim($v); - } - $child_key_value = $v; - $uuid_exists = true; - break; - } - } - else { - $uuid_exists = false; - } - } + //save the message + $this->message = $message; - //allow characters found in the uuid only - if (isset($child_key_value)) { - $child_key_value = self::sanitize($child_key_value); - } + //commit the atomic transaction + $this->db->commit(); - //get the child field names - $child_field_names = array(); - if (is_array($row)) { - foreach ($row as $k => $v) { - if (!is_array($v) && $k !== 'checked') { - $child_field_names[] = self::sanitize($k); - } - } - } - - //determine sql update or delete and get the original data - if ($uuid_exists) { - $sql = "SELECT ". implode(", ", $child_field_names)." FROM ".$child_table_name." "; - $sql .= "WHERE ".$child_key_name." = '".$child_key_value."'; "; - try { - $prep_statement = $this->db->prepare($sql); - if ($prep_statement) { - //get the data - $prep_statement->execute(); - $child_results = $prep_statement->fetch(PDO::FETCH_ASSOC); - - //set the action - if (is_array($child_results)) { - $action = "update"; - } - else { - $action = "add"; - } - } - unset($prep_statement); - } - catch(PDOException $e) { - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; - } - - } - else { - $action = "add"; - } - - //update the child data - if ($action == "update") { - if (permission_exists($child_name.'_edit')) { - - //validate changes - $data_modified = false; - if (is_array($row)) { - foreach ($row as $k => $v) { - //sanitize the key - $k = self::sanitize($k); - - //get the variable type of the value - $database_field_type = gettype($child_results[$k]); - $user_field_type = gettype($v); - - //trim the string - if ($user_field_type === 'string') { - $v = trim($v); - } - - //normalize the data to match the database - if ($database_field_type !== $user_field_type) { - //normalize null - if ($v === '') { - $v = null; - } - - //normalize string - if ($database_field_type === 'string') { - $v = (string)$v; - } - - //normalize numeric - if ($database_field_type === 'numeric') { - $v = intval($v); - } - - //normalize boolean - if ($database_field_type === 'boolean') { - if ($v === 'true') { - $v = true; - } else { - $v = false; - } - } - } - - //verify if the data in the database has been modified - if ($child_results[$k] !== $v) { - //not matched - //echo "$child_name.$k ".($child_results[$k])." != ".$v."\n\n"; - $data_modified = true; - break; - } - } - } - - //child data - process the modified data - if ($data_modified) { - - //update the special values - if (is_array($row)) { - foreach ($row as $k => $v) { - //sanitize the key - $k = self::sanitize($k); - - //save the key value pairs to the temp_array - if (!isset($v) || (isset($v) && $v == '')) { - $temp_array[$k] = null; - } - elseif ($v === "now()") { - $temp_array[$k] = 'now()'; - } - elseif ($v === "user_uuid()") { - $temp_array[$k] = $this->user_uuid ?? null; - } - elseif ($v === "remote_address()") { - $temp_array[$k] = $_SERVER['REMOTE_ADDR']; - } - if (gettype($v) === 'boolean') { - if ($v) { - $v = true; - } else { - $v = false; - } - $temp_array[$k] = $v; - } - else { - if (gettype($v) === 'string') { - $v = trim($v); - } - $temp_array[$k] = $v; - } - } - } - - //add to the old and new arrays - $old_array[$key][] = $child_results; - $new_array[$key][] = $temp_array; - - //empty the temp array - unset($temp_array); - - //update the child data - $sql = "UPDATE ".$child_table_name." SET "; - if (is_array($row)) { - foreach ($row as $k => $v) { - if (!is_array($v) && ($k != $parent_key_name || $k != $child_key_name)) { - $k = self::sanitize($k); - if (!isset($v) || (isset($v) && $v == '')) { - $sql .= $k." = null, "; - } - elseif ($v === "now()") { - $sql .= $k." = now(), "; - } - elseif ($v === "user_uuid()") { - $sql .= $k." = :".$k.", "; - $params[$k] = $this->user_uuid ?? null; - } - elseif ($v === "remote_address()") { - $sql .= $k." = :".$k.", "; - $params[$k] = $_SERVER['REMOTE_ADDR']; - } - elseif (gettype($v) === 'boolean') { - $sql .= $k." = :".$k.", "; - $params[$k] = $v; - } - else { - $sql .= $k." = :".$k.", "; - if (gettype($v) === 'string') { - $v = trim($v); - } - $params[$k] = $v; - } - } - } - } - - //add the modified date and user - $sql .= "update_date = now(), "; - $sql .= "update_user = :update_user "; - $params['update_user'] = $this->user_uuid ?? null; - - //add the where with the parent name and value - $sql .= "WHERE ".$parent_key_name." = :parent_key_value "; - $sql .= "AND ".$child_key_name." = :child_key_value; "; - $params['parent_key_value'] = $parent_key_value; - $params['child_key_value'] = $child_key_value; - $sql = str_replace(", WHERE", " WHERE", $sql); - - //set the error mode - $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - //reduce prepared statement latency - if (defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { - $this->db->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); - } - - try { - $prep_statement = $this->db->prepare($sql); - $prep_statement->execute($params); - unset($prep_statement); - $message["details"][$m]["name"] = $key; - $message["details"][$m]["message"] = "OK"; - $message["details"][$m]["code"] = "200"; - $message["details"][$m]["uuid"] = $child_key_value; - $message["details"][$m]["sql"] = $sql; - if (is_array($params)) { - $message["details"][$m]["params"] = $params; - } - unset($params); - $this->message = $message; - $m++; - } - catch(PDOException $e) { - $retval = false; - if ($message["code"] == "200") { - $message["message"] = "Bad Request"; - $message["code"] = "400"; - } - $message["details"][$m]["name"] = $key; - $message["details"][$m]["message"] = $e->getMessage(); - $message["details"][$m]["code"] = "400"; - $message["details"][$m]["sql"] = $sql; - if (is_array($params)) { - $message["details"][$m]["params"] = $params; - } - unset($params); - $this->message = $message; - $m++; - } - - } - else { - $message["details"][$m]["name"] = $key; - $message["details"][$m]["message"] = 'No Changes'; - $message["details"][$m]["code"] = "000"; - $message["details"][$m]["uuid"] = $child_key_value; - $this->message = $message; - $m++; - } - } - else { - $retval = false; - $message["name"] = $child_name; - $message["message"] = "Forbidden, does not have '".$child_name."_edit'"; - $message["code"] = "403"; - $message["line"] = __line__; - $this->message = $message; - $m++; - } - } //action update - - //add the child data - if ($action == "add") { - if (permission_exists($child_name.'_add')) { - //determine if child or parent key exists - $child_key_name = $child_name.'_uuid'; - $parent_key_exists = false; - $child_key_exists = false; - if (is_array($row)) { - foreach ($row as $k => $v) { - if ($k == $parent_key_name) { - $parent_key_exists = true; - } - if ($k == $child_key_name) { - $child_key_exists = true; - if (gettype($v) === 'string') { - $v = trim($v); - } - $child_key_value = $v; - } - } - } - if (!isset($child_key_value) || $child_key_value == '') { - $child_key_value = uuid(); - } - - //add to the old and new arrays - $old_array = null; - $new_array[$child_name][] = $row; - - //build the insert - $sql = "INSERT INTO ".$child_table_name." "; - $sql .= "("; - if (!$parent_key_exists) { - $sql .= self::singular($parent_key_name).", "; - } - if (!$child_key_exists) { - $sql .= self::singular($child_key_name).", "; - } - if (is_array($row)) { - foreach ($row as $k => $v) { - if (!is_array($v)) { - $k = self::sanitize($k); - if ($k != 'insert_user' && - $k != 'insert_date' && - $k != 'update_user' && - $k != 'update_date') { - $sql .= $k.", "; - } - } - } - } - $sql .= "insert_date, "; - $sql .= "insert_user "; - $sql .= ") "; - $sql .= "VALUES "; - $sql .= "("; - if (!$parent_key_exists) { - $sql .= ":parent_key_value, "; - $params['parent_key_value'] = $parent_key_value; - } - if (!$child_key_exists) { - $sql .= ":child_key_value, "; - $params['child_key_value'] = $child_key_value; - } - if (is_array($row)) { - foreach ($row as $k => $v) { - if (!is_array($v)) { - if ($k != 'insert_user' && - $k != 'insert_date' && - $k != 'update_user' && - $k != 'update_date') { - if (!isset($v) || strlen($v) == 0) { - $sql .= "null, "; - } - elseif ($v === "now()") { - $sql .= "now(), "; - } - elseif ($v === "user_uuid()") { - $sql .= ':'.$k.", "; - $params[$k] = $this->user_uuid ?? null; - } - elseif ($v === "remote_address()") { - $sql .= ':'.$k.", "; - $params[$k] = $_SERVER['REMOTE_ADDR']; - } - elseif (gettype($v) === 'boolean') { - $sql .= ':'.$k.", "; - $params[$k] = $v; - } - else { - $k = self::sanitize($k); - if ($k != 'insert_user' && - $k != 'insert_date' && - $k != 'update_user' && - $k != 'update_date') { - $sql .= ':'.$k.", "; - if (gettype($v) === 'string') { - $v = trim($v); - } - $params[$k] = $v; - } - } - } - } - } - } - $sql .= "now(), "; - $sql .= ":insert_user "; - $sql .= ");"; - - //add insert user parameter - $params['insert_user'] = $this->user_uuid ?? null; - - //set the error mode - $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - //reduce prepared statement latency - if (defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { - $this->db->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); - } - - //run the query and return the results - try { - $prep_statement = $this->db->prepare($sql); - $prep_statement->execute($params); - unset($prep_statement); - $message["code"] = "200"; - $message["details"][$m]["name"] = $key; - $message["details"][$m]["message"] = "OK"; - $message["details"][$m]["code"] = "200"; - $message["details"][$m]["uuid"] = $child_key_value; - $message["details"][$m]["sql"] = $sql; - if (is_array($params)) { - $message["details"][$m]["params"] = $params; - } - unset($params); - $this->message = $message; - $m++; - } - catch(PDOException $e) { - $retval = false; - if ($message["code"] == "200") { - $message["message"] = "Bad Request"; - $message["code"] = "400"; - } - $message["details"][$m]["name"] = $key; - $message["details"][$m]["message"] = $e->getMessage(); - $message["details"][$m]["code"] = "400"; - $message["details"][$m]["sql"] = $sql; - if (is_array($params)) { - $message["details"][$m]["params"] = $params; - } - unset($params); - $this->message = $message; - $m++; - } - } - else { - $retval = false; - $message["name"] = $child_name; - $message["message"] = "Forbidden, does not have '".$child_name."_add'"; - $message["code"] = "403"; - $message["line"] = __line__; - $this->message = $message; - $m++; - } - } //action add - - //unset the variables - unset($sql, $action, $child_key_name, $child_key_value); - } // foreach value - - } //is array - } //foreach array - } - - } // foreach schema_array - } // foreach main array - - //save the message - $this->message = $message; - - //commit the atomic transaction - $this->db->commit(); - - } catch (\PDOException $e) { - //rollback the transaction on error - if ($this->db->inTransaction()) { - $this->db->rollback(); - } - - //prepare the message array - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; + } catch (PDOException $e) { + //rollback the transaction on error + if ($this->db->inTransaction()) { + $this->db->rollback(); } - //set the action if not set - if (empty($action)) { - if (!empty($old_array)) { - $transaction_type = 'update'; - } - else { - $transaction_type = 'add'; - } - } - else { - $transaction_type = $action; - } - - //debug message - //echo "old\n"; - //view_array($old_array, false); - //echo "new\n"; - //view_array($new_array, false); - //exit; - - //check to see if the database was updated; update the message code if needed - $database_updated = false; - if (!empty($this->message['code']) && $this->message['code'] === '200') { - $database_updated = true; - } - if (!$database_updated) { - foreach($this->message['details'] as $row) { - if ($row['code'] === '200') { - $database_updated = true; - $message["code"] = '200'; - break; - } - } - } - - //log the transaction results - if ($transaction_save && $database_updated && file_exists($_SERVER["PROJECT_ROOT"]."/app/database_transactions/app_config.php")) { - try { - //build the json string from the array - if (!empty($old_array)) { - $old_json = json_encode($old_array, JSON_PRETTY_PRINT); - } - if (!empty($new_array)) { - $new_json = json_encode($new_array, JSON_PRETTY_PRINT); - } - - //insert the transaction into the database - $sql = "insert into ".self::TABLE_PREFIX."database_transactions "; - $sql .= "("; - $sql .= "database_transaction_uuid, "; - if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { - $sql .= "domain_uuid, "; - } - if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { - $sql .= "user_uuid, "; - } - if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { - $sql .= "app_uuid, "; - } - if (isset($this->app_name) && !empty($this->app_name)) { - $sql .= "app_name, "; - } - $sql .= "transaction_code, "; - $sql .= "transaction_address, "; - $sql .= "transaction_type, "; - $sql .= "transaction_date, "; - $sql .= "transaction_old, "; - $sql .= "transaction_new, "; - $sql .= "transaction_result "; - $sql .= ")"; - $sql .= "values "; - $sql .= "("; - $sql .= "'".uuid()."', "; - if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { - $sql .= ":domain_uuid, "; - } - if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { - $sql .= ":user_uuid, "; - } - if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { - $sql .= ":app_uuid, "; - } - if (isset($this->app_name) && !empty($this->app_name)) { - $sql .= ":app_name, "; - } - $sql .= "'".$message["code"]."', "; - $sql .= ":remote_address, "; - $sql .= "'".$transaction_type."', "; - $sql .= "now(), "; - if (!empty($old_json)) { - $sql .= ":transaction_old, "; - } - else { - $sql .= "null, "; - } - if (!empty($new_json)) { - $sql .= ":transaction_new, "; - } - else { - $sql .= "null, "; - } - $sql .= ":transaction_result "; - $sql .= ")"; - $statement = $this->db->prepare($sql); - if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { - $statement->bindParam(':domain_uuid', $this->domain_uuid); - } - if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { - $statement->bindParam(':user_uuid', $this->user_uuid); - } - if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { - $statement->bindParam(':app_uuid', $this->app_uuid); - } - if (isset($this->app_name) && !empty($this->app_name)) { - $statement->bindParam(':app_name', $this->app_name); - } - $statement->bindParam(':remote_address', $_SERVER['REMOTE_ADDR']); - if (!empty($old_json)) { - $old_json = json_encode($old_array, JSON_PRETTY_PRINT); - $statement->bindParam(':transaction_old', $old_json); - } - if (!empty($new_json)) { - $statement->bindParam(':transaction_new', $new_json); - } - $message = json_encode($this->message, JSON_PRETTY_PRINT); - $statement->bindParam(':transaction_result', $message); - $statement->execute(); - unset($sql, $old_array, $old_json, $new_array, $new_json); - } - catch(PDOException $e) { - $message['message'] = $e->getMessage(); - $message['code'] = $e->getCode(); - $message['line'] = $e->getLine(); - $message['file'] = $e->getFile(); - $message['trace'] = $e->getTraceAsString(); - $message['debug'] = debug_backtrace(); - $this->message = $message; - return false; - } - } - return $this->message; - } //save method - - /** - * Ensure the database is still connected and active. - *

    NOTE:
    - * There is no method in PDO that can reliably detect if the connection is active. Therefor, a lightweight - * query is executed using the statement select 1.

    - * @return bool True if the database is connected. False otherwise. - */ - public function is_connected(): bool { - try { - $stmt = false; - if ($this->db !== null) $stmt = $this->db->query('SELECT 1'); - return $stmt !== false; - } catch (PDOException $ex) { - //database is not connected - return false; - } catch (Exception $e) { - //some other error has occurred so record it + //prepare the message array $message['message'] = $e->getMessage(); $message['code'] = $e->getCode(); $message['line'] = $e->getLine(); @@ -3231,222 +3234,319 @@ class database { $this->message = $message; return false; } - return true; + + //set the action if not set + if (empty($action)) { + if (!empty($old_array)) { + $transaction_type = 'update'; + } else { + $transaction_type = 'add'; + } + } else { + $transaction_type = $action; + } + + //debug message + //echo "old\n"; + //view_array($old_array, false); + //echo "new\n"; + //view_array($new_array, false); + //exit; + + //check to see if the database was updated; update the message code if needed + $database_updated = false; + if (!empty($this->message['code']) && $this->message['code'] === '200') { + $database_updated = true; + } + if (!$database_updated) { + foreach ($this->message['details'] as $row) { + if ($row['code'] === '200') { + $database_updated = true; + $message["code"] = '200'; + break; + } + } + } + + //log the transaction results + if ($transaction_save && $database_updated && file_exists($_SERVER["PROJECT_ROOT"] . "/app/database_transactions/app_config.php")) { + try { + //build the json string from the array + if (!empty($old_array)) { + $old_json = json_encode($old_array, JSON_PRETTY_PRINT); + } + if (!empty($new_array)) { + $new_json = json_encode($new_array, JSON_PRETTY_PRINT); + } + + //insert the transaction into the database + $sql = "insert into " . self::TABLE_PREFIX . "database_transactions "; + $sql .= "("; + $sql .= "database_transaction_uuid, "; + if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { + $sql .= "domain_uuid, "; + } + if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { + $sql .= "user_uuid, "; + } + if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { + $sql .= "app_uuid, "; + } + if (isset($this->app_name) && !empty($this->app_name)) { + $sql .= "app_name, "; + } + $sql .= "transaction_code, "; + $sql .= "transaction_address, "; + $sql .= "transaction_type, "; + $sql .= "transaction_date, "; + $sql .= "transaction_old, "; + $sql .= "transaction_new, "; + $sql .= "transaction_result "; + $sql .= ")"; + $sql .= "values "; + $sql .= "("; + $sql .= "'" . uuid() . "', "; + if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { + $sql .= ":domain_uuid, "; + } + if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { + $sql .= ":user_uuid, "; + } + if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { + $sql .= ":app_uuid, "; + } + if (isset($this->app_name) && !empty($this->app_name)) { + $sql .= ":app_name, "; + } + $sql .= "'" . $message["code"] . "', "; + $sql .= ":remote_address, "; + $sql .= "'" . $transaction_type . "', "; + $sql .= "now(), "; + if (!empty($old_json)) { + $sql .= ":transaction_old, "; + } else { + $sql .= "null, "; + } + if (!empty($new_json)) { + $sql .= ":transaction_new, "; + } else { + $sql .= "null, "; + } + $sql .= ":transaction_result "; + $sql .= ")"; + $statement = $this->db->prepare($sql); + if (isset($this->domain_uuid) && is_uuid($this->domain_uuid)) { + $statement->bindParam(':domain_uuid', $this->domain_uuid); + } + if (isset($this->user_uuid) && is_uuid($this->user_uuid)) { + $statement->bindParam(':user_uuid', $this->user_uuid); + } + if (isset($this->app_uuid) && is_uuid($this->app_uuid)) { + $statement->bindParam(':app_uuid', $this->app_uuid); + } + if (isset($this->app_name) && !empty($this->app_name)) { + $statement->bindParam(':app_name', $this->app_name); + } + $statement->bindParam(':remote_address', $_SERVER['REMOTE_ADDR']); + if (!empty($old_json)) { + $old_json = json_encode($old_array, JSON_PRETTY_PRINT); + $statement->bindParam(':transaction_old', $old_json); + } + if (!empty($new_json)) { + $statement->bindParam(':transaction_new', $new_json); + } + $message = json_encode($this->message, JSON_PRETTY_PRINT); + $statement->bindParam(':transaction_result', $message); + $statement->execute(); + unset($sql, $old_array, $old_json, $new_array, $new_json); + } catch (PDOException $e) { + $message['message'] = $e->getMessage(); + $message['code'] = $e->getCode(); + $message['line'] = $e->getLine(); + $message['file'] = $e->getFile(); + $message['trace'] = $e->getTraceAsString(); + $message['debug'] = debug_backtrace(); + $this->message = $message; + return false; + } + } + return $this->message; } - /** - * Converts a plural English word to singular. - * @param string $word English word - * @return string Singular version of English word - * @internal Moved to class to conserve resources. +/** + * Toggles fields on a table using the toggle_field array values within the app object. + * + * @param array $array Three dimensional array. The first dimension is the table name without the prefix 'v_'. + * Second dimension in the row value as int. Third dimension is the column name. + * + * @return bool Returns true on success and false on failure. + * @depends database::save() + * @depends database::get_apps() */ - public static function singular(string $word) { - //"-es" is used for words that end in "-x", "-s", "-z", "-sh", "-ch" in which case you add - if (substr($word, -2) == "es") { - if (substr($word, -4) == "sses") { // eg. 'addresses' to 'address' - return substr($word,0,-2); - } - elseif (substr($word, -3) == "ses") { // eg. 'databases' to 'database' (necessary!) - return substr($word,0,-1); - } - elseif (substr($word, -3) == "ies") { // eg. 'countries' to 'country' - return substr($word,0,-3)."y"; - } - elseif (substr($word, -3, 1) == "x") { - return substr($word,0,-2); - } - elseif (substr($word, -3, 1) == "s") { - return substr($word,0,-2); - } - elseif (substr($word, -3, 1) == "z") { - return substr($word,0,-2); - } - elseif (substr($word, -4, 2) == "sh") { - return substr($word,0,-2); - } - elseif (substr($word, -4, 2) == "ch") { - return substr($word,0,-2); - } - else { - return rtrim($word, "s"); + public function toggle(array $array) { + + //return the array + if (!is_array($array)) { + return false; + } + + //set the message id + $m = 0; + + //loop through the array + if (!empty($array) && is_array($array)) { + $x = 0; + foreach ($array as $parent_name => $tables) { + if (!empty($tables) && is_array($tables)) { + foreach ($tables as $id => $row) { + + //prepare the variables + $parent_name = self::sanitize($parent_name); + $parent_key_name = self::singular($parent_name) . "_uuid"; + + //build the toggle array + if (!empty($row['checked']) && $row['checked'] == 'true') { + //toggle the field value + //$toggle_array[$parent_name][$x][$parent_key_name] = $row[$parent_key_name]; + $toggle_array[$parent_name][$x] = $row; + + //remove the row from the main array + unset($array[$parent_name][$x]); + } + + //loop through the fields + foreach ($row as $field_name => $field_value) { + + //find the child tables + $y = 0; + if (!empty($field_value) && is_array($field_value)) { + //prepare the variables + $child_name = self::sanitize($field_name); + $child_key_name = self::singular($child_name) . "_uuid"; + + //loop through the child rows + foreach ($field_value as $sub_row) { + + //build the delete array + if ($sub_row['checked'] == 'true') { + //delete the child data + $delete_array[$child_name][$y][$child_key_name] = $sub_row[$child_key_name]; + + //remove the row from the main array + unset($array[$parent_name][$x][$child_name][$y]); + } + + //increment the value + $y++; + } + } + } + + //increment the value + $x++; + + } + } } } - else { - return rtrim($word, "s"); + + //unset the original array + unset($array); + + //get the $apps array from the installed apps from the core and mod directories + if (count(self::$apps) == 0) { + self::get_apps(); } + + //search through all fields to see if toggle field exists + foreach (self::$apps as $x => $app) { + if (!empty($app['db']) && is_array($app['db'])) { + foreach ($app['db'] as $y => $row) { + if (is_array($row['table']['name'])) { + $table_name = $row['table']['name']['text']; + } else { + $table_name = $row['table']['name']; + } + if ($table_name === self::TABLE_PREFIX . $parent_name) { + if (is_array($row['fields'])) { + foreach ($row['fields'] as $field) { + if (isset($field['toggle'])) { + $toggle_field = $field['name']; + $toggle_values = $field['toggle']; + } + } + } + } + } + } + } + + //if the toggle field and values are empty then set defaults + if (empty($toggle_field)) { + $toggle_field = self::singular($parent_name) . "_enabled"; + } + if (empty($toggle_values)) { + $toggle_values[] = 'true'; + $toggle_values[] = 'false'; + } + + //get the current values from the database + foreach ($toggle_array as $table_name => $table) { + $x = 0; + foreach ($table as $row) { + $child_name = self::sanitize($table_name); + $child_key_name = self::singular($child_name) . "_uuid"; + + $array[$table_name][$x][$child_key_name] = $row[$child_key_name]; + $array[$table_name][$x][$toggle_field] = ($row[$toggle_field] === $toggle_values[0]) ? $toggle_values[1] : $toggle_values[0]; + $x++; + } + } + unset($toggle_array); + + //save the array + return $this->save($array); + } /** - * Gets the $apps array from the installed apps from the core and mod directories and writes it to self::$apps overwriting previous values. - * @uses $_SERVER['DOCUMENT_ROOT'] Global variable - * @uses PROJECT_PATH Global variable + * Gets the $apps array from the installed apps from the core and mod directories and writes it to self::$apps + * overwriting previous values. + * * @return null Does not return any values + * @uses PROJECT_PATH Global variable + * @uses $_SERVER['DOCUMENT_ROOT'] Global variable * @internal Moved to class to conserve resources. */ public static function get_apps() { //get the $apps array from the installed apps from the core and mod directories - $config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php"); - $x = 0; - if (is_array($config_list)) { - foreach ($config_list as $config_path) { - include($config_path); - $x++; - } - } - self::$apps = $apps; - } - - /** - * Returns the depth of an array - * @param array $array Reference to array - * @return int Depth of array - * @internal Moved to class to conserve resources. - */ - public static function array_depth(array &$array) { - $depth = 0; - if (is_array($array)) { - $depth++; - foreach ($array as $value) { - if (is_array($value)) { - $depth = self::array_depth($value) + 1; - } - } - } - return $depth; - } - - /** - * Searches through all fields to see if domain_uuid exists - * @param string $name - * @uses self::$apps directly - * @return boolean true on success and false on failure - * @see database::get_apps() - */ - public static function domain_uuid_exists($name) { - //get the $apps array from the installed apps from the core and mod directories - if (count(self::$apps) == 0) { - self::get_apps(); - } - - //search through all fields to see if domain_uuid exists - foreach (self::$apps as $x => &$app) { - if (is_array($app['db'])) { - foreach ($app['db'] as $y => $row) { - if (is_array($row['table']['name'])) { - $table_name = $row['table']['name']['text']; - } - else { - $table_name = $row['table']['name']; - } - if ($table_name === self::TABLE_PREFIX.$name) { - if (is_array($row['fields'])) { - foreach ($row['fields'] as $field) { - if ($field['name'] == "domain_uuid") { - return true; - } - } //foreach - } //is array - } - } //foreach - } //is array - } //foreach - - //not found - return false; - } - - /** - * Get Relations searches through all fields to find relations - * @param string $schema Table name - * @return array Returns array or false - * @internal Moved to class to conserve resources. - */ - public static function get_relations($schema) { - - //remove the v_ prefix - if (substr($schema, 0, strlen(self::TABLE_PREFIX)) == self::TABLE_PREFIX) { - $schema = substr($schema, strlen(self::TABLE_PREFIX)); - } - - //sanitize the values - $schema = self::sanitize($schema); - - //get the apps array - $config_list = []; - $directories = ["core", "app"]; - $applications = [$schema, self::singular($schema)]; - foreach ($directories as $directory) { - foreach ($applications as $application) { - $path = $_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/$directory/$application/app_config.php"; - $app_config_files = glob($path); - if ($app_config_files !== false) { - $config_list = array_merge($config_list, $app_config_files); - } - } - } - $x = 0; + $config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php"); + $x = 0; + if (is_array($config_list)) { foreach ($config_list as $config_path) { include($config_path); $x++; } - - //search through all fields to find relations - if (!empty($apps) && is_array($apps)) { - foreach ($apps as $x => $app) { - foreach ($app['db'] as $y => $row) { - foreach ($row['fields'] as $z => $field) { - if (!empty($field['deprecated']) && $field['deprecated'] != "true") { - if (!empty($field['key']['type']) && $field['key']['type'] == "foreign") { - if ($row['table']['name'] == self::TABLE_PREFIX.$schema || $field['key']['reference']['table'] == self::TABLE_PREFIX.$schema) { - //get the field name - if (!empty($field['name']) && is_array($field['name'])) { - $field_name = trim($field['name']['text']); - } - else { - $field_name = trim($field['name']); - } - //build the array - $relations[$i]['table'] = $row['table']['name']; - $relations[$i]['field'] = $field_name; - $relations[$i]['key']['type'] = $field['key']['type']; - $relations[$i]['key']['table'] = $field['key']['reference']['table']; - $relations[$i]['key']['field'] = $field['key']['reference']['field']; - if (isset($field['key']['reference']['action'])) { - $relations[$i]['key']['action'] = $field['key']['reference']['action']; - } - //increment the value - $i++; - } - } - } - unset($field_name); - } - } - } - } - - //return the array - if (!empty($relations) && is_array($relations)) { - return $relations; - } else { - return false; - } + } + self::$apps = $apps; } /** * Gets a list of database views from the file system. + * * @param string $action options: list, create, drop + * * @return array shows list of views, list of views that were updated */ public function views(string $action) { $files = glob(dirname(__DIR__, 2) . "/*/*/resources/database/views/*.php"); - foreach($files as $id => $file) { - $view = array(); + foreach ($files as $id => $file) { + $view = []; try { include $file; $views[$id] = $view; - } catch (\Exception $e) { + } catch (Exception $e) { $views[$id]['error'] = $e->getMessage(); } finally { $views[$id]['file'] = $file; @@ -3460,8 +3560,8 @@ class database { //update views if ($action === 'create') { - $array = array(); - foreach($views as $id => $row) { + $array = []; + foreach ($views as $id => $row) { if (!empty($row['name']) && !empty($row['sql'])) { //set the variables $view_name = $row['name']; @@ -3471,15 +3571,14 @@ class database { //$view_description = $row['description']; //create and run the view sql - $sql = "CREATE OR REPLACE VIEW ".$view_name." AS (\n"; - $sql .= $view_sql."\n"; + $sql = "CREATE OR REPLACE VIEW " . $view_name . " AS (\n"; + $sql .= $view_sql . "\n"; $sql .= ")\n"; $this->execute($sql); //build the return array $views[$id]['result'] = $this->message; - } - else { + } else { //build the return array $views[$id]['result'] = 'Name or SQL empty'; } @@ -3491,20 +3590,19 @@ class database { //drop views if ($action === 'drop') { - $array = array(); - foreach($views as $id => $row) { + $array = []; + foreach ($views as $id => $row) { if (!empty($row['name'])) { //set the variables $view_name = $row['name']; //create and run the view sql - $sql = "DROP VIEW ".$view_name.";"; + $sql = "DROP VIEW " . $view_name . ";"; $this->execute($sql); //build the return array $views[$id]['result'] = 'Dropped'; - } - else { + } else { //build the return array $views[$id]['result'] = 'Name or SQL empty'; } @@ -3515,90 +3613,38 @@ class database { } } - /** - * Returns a sanitized string value safe for database or table name. - * @param string $value To be sanitized - * @return string Sanitized using preg_replace('#[^a-zA-Z0-9_\-]#', '') - * @see preg_replace() - */ - public static function sanitize(string $value) { - return preg_replace('#[^a-zA-Z0-9_\-]#', '', $value); - } - - /** - * Returns a new connected database object.
    - *

    This allows a shortcut for a common syntax. For more information - * on how the connection happens see {@link database::__construct()} and - * {@link database::connect()}

    - *

    Usage:
    - *   $database_object = database::new();

    - * @return database new instance of database object already connected - * @see database::__construct() - * @see database::connect() - */ - public static function new(array $params = []) { - - //re-use the database connection - if (self::$database === null) { - self::$database = new database($params); - if (!self::$database->is_connected()) { - self::$database->connect(); - } - } - - //set the user_uuid - if (!empty($params['user_uuid'])) { - //use the parameter as the first priority when available - self::$database->user_uuid = $params['user_uuid']; - } elseif (!empty($_SESSION['user_uuid'])) { - //use the session when available - self::$database->user_uuid = $_SESSION['user_uuid']; - } - - //set the domain_uuid - if (!empty($params['domain_uuid'])) { - //use the parameter as the first priority when available - self::$database->domain_uuid = $params['domain_uuid']; - } elseif (!empty($_SESSION['domain_uuid'])) { - //use the session when available - self::$database->domain_uuid = $_SESSION['domain_uuid']; - } - - return self::$database; - } - } //class database //addtitional functions for sqlite - if (!function_exists('php_md5')) { - function php_md5($string) { - return md5($string); - } +if (!function_exists('php_md5')) { + function php_md5($string) { + return md5($string); } +} - if (!function_exists('php_unix_time_stamp')) { - function php_unix_time_stamp($string) { - return strtotime($string); - } +if (!function_exists('php_unix_time_stamp')) { + function php_unix_time_stamp($string) { + return strtotime($string); } +} - if (!function_exists('php_now')) { - function php_now() { - return date("Y-m-d H:i:s"); - } +if (!function_exists('php_now')) { + function php_now() { + return date("Y-m-d H:i:s"); } +} - if (!function_exists('php_left')) { - function php_left($string, $num) { - return substr($string, 0, $num); - } +if (!function_exists('php_left')) { + function php_left($string, $num) { + return substr($string, 0, $num); } +} - if (!function_exists('php_right')) { - function php_right($string, $num) { - return substr($string, (strlen($string)-$num), strlen($string)); - } +if (!function_exists('php_right')) { + function php_right($string, $num) { + return substr($string, (strlen($string) - $num), strlen($string)); } +} /* //example usage diff --git a/resources/classes/domains.php b/resources/classes/domains.php index 4d3161bb60..9407ada73b 100644 --- a/resources/classes/domains.php +++ b/resources/classes/domains.php @@ -38,8 +38,8 @@ class domains { const app_uuid = '8b91605b-f6d2-42e6-a56d-5d1ded01bb44'; /** - * declare the variables - */ + * declare the variables + */ private $name; private $table; private $toggle_field; @@ -48,37 +48,47 @@ class domains { /** * Set in the constructor. Must be a database object and cannot be null. + * * @var database Database Object */ private $database; /** * Settings object set in the constructor. Must be a settings object and cannot be null. + * * @var settings Settings Object */ private $settings; /** - * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array + * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in + * the session global array + * * @var string */ private $user_uuid; /** - * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array + * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set + * in the session global array + * * @var string */ private $domain_uuid; /** - * called when the object is created + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $setting_array An optional array of settings to override default values. Defaults to []. */ public function __construct($setting_array = []) { //assign the variables $this->name = 'domain'; $this->table = 'domains'; $this->toggle_field = 'domain_enabled'; - $this->toggle_values = ['true','false']; + $this->toggle_values = ['true', 'false']; $this->location = 'domains.php'; //set the domain and user uuids @@ -101,750 +111,775 @@ class domains { } /** - * delete rows from the database + * get all enabled domains + * + * @returns array all enabled domains with uuid as array key */ - public function delete($records) { - if (permission_exists($this->name.'_delete')) { + public static function enabled() { - //add multi-lingual support - $language = new text; - $text = $language->get(); + //define database as global + global $database; - //validate the token - $token = new token; - if (!$token->validate($_SERVER['PHP_SELF'])) { - message::add($text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } + //define default return value + $domains = []; - //delete multiple records - if (is_array($records) && @sizeof($records) != 0) { - //build the delete array - foreach ($records as $record) { - //add to the array - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - //set the uuid - $id = $record['uuid']; - - //get the domain using the id - $sql = "select domain_name from v_domains "; - $sql .= "where domain_uuid = :domain_uuid "; - $parameters['domain_uuid'] = $id; - $domain_name = $this->database->select($sql, $parameters, 'column'); - unset($sql, $parameters); - - //get the domain settings - $sql = "select * from v_domain_settings "; - $sql .= "where domain_uuid = :domain_uuid "; - $sql .= "and domain_setting_enabled = 'true' "; - $parameters['domain_uuid'] = $id; - $result = $this->database->select($sql, $parameters, 'all'); - unset($sql, $parameters); - - if (is_array($result) && sizeof($result) != 0) { - foreach ($result as $row) { - $name = $row['domain_setting_name']; - $category = $row['domain_setting_category']; - $subcategory = $row['domain_setting_subcategory']; - if ($subcategory != '') { - if ($name == "array") { - $_SESSION[$category][] = $row['default_setting_value']; - } - else { - $_SESSION[$category][$name] = $row['default_setting_value']; - } - } - else { - if ($name == "array") { - $_SESSION[$category][$subcategory][] = $row['default_setting_value']; - } - else { - $_SESSION[$category][$subcategory]['uuid'] = $row['default_setting_uuid']; - $_SESSION[$category][$subcategory][$name] = $row['default_setting_value']; - } - } - } - } - unset($result, $row); - - //get the $apps array from the installed apps from the core and mod directories - $config_list = glob($_SERVER["DOCUMENT_ROOT"].PROJECT_PATH."/*/*/app_config.php"); - $x=0; - if (isset($config_list)) foreach ($config_list as $config_path) { - include($config_path); - $x++; - } - - //delete the domain data from all tables in the database - if (isset($apps)) foreach ($apps as $app) { - if (isset($app['db'])) foreach ($app['db'] as $row) { - if (is_array($row['table']['name'])) { - $table_name = $row['table']['name']['text']; - if (defined('STDIN')) { - echo "
    ".print_r($table_name, 1)."
    \n";
    -													}
    -												}
    -												else {
    -													$table_name = $row['table']['name'];
    -												}
    -												if ($table_name !== "v" && isset($row['fields'])) {
    -													foreach ($row['fields'] as $field) {
    -														if ($field['name'] == 'domain_uuid' && $table_name != 'v_domains') {
    -															$sql = "delete from ".$table_name." where domain_uuid = :domain_uuid ";
    -															$parameters['domain_uuid'] = $id;
    -															$this->database->app_name = 'domain_settings';
    -															$this->database->app_uuid = 'b31e723a-bf70-670c-a49b-470d2a232f71';
    -															$this->database->execute($sql, $parameters);
    -															unset($sql, $parameters);
    -														}
    -													}
    -												}
    -											}
    -										}
    -
    -									//delete the directories
    -										if (!empty($domain_name)) {
    -											//set the needle
    -											if (count($_SESSION["domains"]) > 1) {
    -												$v_needle = 'v_'.$domain_name.'_';
    -											}
    -											else {
    -												$v_needle = 'v_';
    -											}
    -
    -											//delete the dialplan
    -											@unlink($_SESSION['switch']['dialplan']['dir'].'/'.$domain_name.'.xml');
    -											if (!empty($_SESSION['switch']['dialplan']['dir'])) {
    -												system('rm -rf '.$_SESSION['switch']['dialplan']['dir'].'/'.$domain_name);
    -											}
    -
    -											//delete the dialplan public
    -											@unlink($_SESSION['switch']['dialplan']['dir'].'/public/'.$domain_name.'.xml');
    -											if (!empty($_SESSION['switch']['dialplan']['dir'])) {
    -												system('rm -rf '.$_SESSION['switch']['dialplan']['dir'].'/public/'.$domain_name);
    -											}
    -
    -											//delete the extension
    -											@unlink($_SESSION['switch']['extensions']['dir'].'/'.$domain_name.'.xml');
    -											if (!empty($_SESSION['switch']['extensions']['dir'])) {
    -												system('rm -rf '.$_SESSION['switch']['extensions']['dir'].'/'.$domain_name);
    -											}
    -
    -											//delete fax
    -											if (!empty($_SESSION['switch']['storage']['dir'])) {
    -												system('rm -rf '.$_SESSION['switch']['storage']['dir'].'/fax/'.$domain_name);
    -											}
    -
    -											//delete the gateways
    -											if (!empty($_SESSION['switch']['sip_profiles']['dir'])) {
    -												if ($dh = opendir($_SESSION['switch']['sip_profiles']['dir'])) {
    -													$files = Array();
    -													while ($file = readdir($dh)) {
    -														if ($file != "." && $file != ".." && $file[0] != '.') {
    -															if (is_dir($dir . "/" . $file)) {
    -																//this is a directory do nothing
    -															}
    -															else {
    -																//check if file extension is xml
    -																if (strpos($file, $v_needle) !== false && substr($file,-4) == '.xml') {
    -																	@unlink($_SESSION['switch']['sip_profiles']['dir']."/".$file);
    -																}
    -															}
    -														}
    -													}
    -													closedir($dh);
    -												}
    -											}
    -
    -											//delete the ivr menu
    -											if (!empty($_SESSION['switch']['conf']['dir'])) {
    -												if ($dh = opendir($_SESSION['switch']['conf']['dir']."/ivr_menus")) {
    -													$files = Array();
    -													while ($file = readdir($dh)) {
    -														if ($file != "." && $file != ".." && $file[0] != '.') {
    -															if (!empty($dir) && !empty($file) && is_dir($dir."/".$file)) {
    -																//this is a directory
    -															}
    -															else {
    -																if (strpos($file, $v_needle) !== false && substr($file,-4) == '.xml') {
    -																	@unlink($_SESSION['switch']['conf']['dir']."/ivr_menus/".$file);
    -																}
    -															}
    -														}
    -													}
    -													closedir($dh);
    -												}
    -											}
    -
    -											//delete the recordings
    -											if (!empty($_SESSION['switch']['recordings']['dir'])) {
    -												system('rm -rf '.$_SESSION['switch']['recordings']['dir'].'/'.$_SESSION['domain_name'].'/'.$domain_name);
    -											}
    -
    -											//delete voicemail
    -											if (!empty($_SESSION['switch']['voicemail']['dir'])) {
    -												system('rm -rf '.$_SESSION['switch']['voicemail']['dir'].'/'.$domain_name);
    -											}
    -										}
    -
    -									//apply settings reminder
    -										$_SESSION["reload_xml"] = true;
    -
    -									//remove the domain from domains session array
    -										unset($_SESSION["domains"][$id]);
    -
    -									//add domain uuid to array for deletion below
    -										$domain_array['domains'][] = ['domain_uuid'=>$id];
    -								}
    -						}
    -
    -					//delete the checked rows
    -						if (is_array($domain_array) && @sizeof($domain_array) != 0) {
    -							//execute delete
    -								$this->database->delete($domain_array);
    -
    -							//set message
    -								message::add($text['message-delete']);
    -
    -							//reload default/domain settings
    -								$this->set();
    -						}
    -						unset($records);
    -				}
    +		//get the domains from the database
    +		$sql = "select * from v_domains ";
    +		$sql .= "where domain_enabled = true ";
    +		$sql .= "order by domain_name asc; ";
    +		$result = $database->select($sql, null, 'all');
    +		if (!empty($result)) {
    +			foreach ($result as $row) {
    +				$domains[$row['domain_uuid']] = $row;
    +			}
     		}
    +
    +		//return the domains array
    +		return $domains;
     	}
     
     	/**
    -	 * toggle a field between two values
    +	 * get all disabled domains
    +	 *
    +	 * @returns array all disabled domains with uuid as array key
    +	 */
    +	public static function disabled() {
    +
    +		//define database as global
    +		global $database;
    +
    +		//define default return value
    +		$domains = [];
    +
    +		//get the domains from the database
    +		$sql = "select * from v_domains ";
    +		$sql .= "where domain_enabled = false ";
    +		$sql .= "order by domain_name asc; ";
    +		$result = $database->select($sql, null, 'all');
    +		if (!empty($result)) {
    +			foreach ($result as $row) {
    +				$domains[$row['domain_uuid']] = $row;
    +			}
    +		}
    +
    +		//return the domains array
    +		return $domains;
    +	}
    +
    +	/**
    +	 * Toggles the state of one or more records.
    +	 *
    +	 * @param array $records  An array of record IDs to delete, where each ID is an associative array
    +	 *                        containing 'uuid' and 'checked' keys. The 'checked' value indicates
    +	 *                        whether the corresponding checkbox was checked for deletion.
    +	 *
    +	 * @return void No return value; this method modifies the database state and sets a message.
     	 */
     	public function toggle($records) {
    -		if (permission_exists($this->name.'_edit')) {
    +		if (permission_exists($this->name . '_edit')) {
     
     			//add multi-lingual support
    -				$language = new text;
    -				$text = $language->get();
    +			$language = new text;
    +			$text = $language->get();
     
     			//validate the token
    -				$token = new token;
    -				if (!$token->validate($_SERVER['PHP_SELF'])) {
    -					message::add($text['message-invalid_token'],'negative');
    -					header('Location: '.$this->location);
    -					exit;
    -				}
    +			$token = new token;
    +			if (!$token->validate($_SERVER['PHP_SELF'])) {
    +				message::add($text['message-invalid_token'], 'negative');
    +				header('Location: ' . $this->location);
    +				exit;
    +			}
     
     			//toggle the checked records
    -				if (is_array($records) && @sizeof($records) != 0) {
    -					//get current toggle state
    -						foreach($records as $record) {
    -							if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) {
    -								$uuids[] = "'".$record['uuid']."'";
    -							}
    -						}
    -						if (is_array($uuids) && @sizeof($uuids) != 0) {
    -							$sql = "select ".$this->name."_uuid as uuid, ".$this->toggle_field." as toggle from v_".$this->table." ";
    -							$sql .= "where ".$this->name."_uuid in (".implode(', ', $uuids).") ";
    -							$rows = $this->database->select($sql, $parameters ?? null, 'all');
    -							if (is_array($rows) && @sizeof($rows) != 0) {
    -								foreach ($rows as $row) {
    -									$states[$row['uuid']] = $row['toggle'];
    -								}
    -							}
    -							unset($sql, $parameters, $rows, $row);
    -						}
    -
    -					//build update array
    -						$x = 0;
    -						foreach($states as $uuid => $state) {
    -							//create the array
    -								$array[$this->table][$x][$this->name.'_uuid'] = $uuid;
    -								$array[$this->table][$x][$this->toggle_field] = $state == $this->toggle_values[0] ? $this->toggle_values[1] : $this->toggle_values[0];
    -
    -							//increment the id
    -								$x++;
    -						}
    -
    -					//save the changes
    -						if (is_array($array) && @sizeof($array) != 0) {
    -							//save the array
    -
    -								$this->database->save($array);
    -								unset($array);
    -
    -							//set message
    -								message::add($text['message-toggle']);
    -						}
    -						unset($records, $states);
    +			if (is_array($records) && @sizeof($records) != 0) {
    +				//get current toggle state
    +				foreach ($records as $record) {
    +					if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) {
    +						$uuids[] = "'" . $record['uuid'] . "'";
    +					}
     				}
    +				if (is_array($uuids) && @sizeof($uuids) != 0) {
    +					$sql = "select " . $this->name . "_uuid as uuid, " . $this->toggle_field . " as toggle from v_" . $this->table . " ";
    +					$sql .= "where " . $this->name . "_uuid in (" . implode(', ', $uuids) . ") ";
    +					$rows = $this->database->select($sql, $parameters ?? null, 'all');
    +					if (is_array($rows) && @sizeof($rows) != 0) {
    +						foreach ($rows as $row) {
    +							$states[$row['uuid']] = $row['toggle'];
    +						}
    +					}
    +					unset($sql, $parameters, $rows, $row);
    +				}
    +
    +				//build update array
    +				$x = 0;
    +				foreach ($states as $uuid => $state) {
    +					//create the array
    +					$array[$this->table][$x][$this->name . '_uuid'] = $uuid;
    +					$array[$this->table][$x][$this->toggle_field] = $state == $this->toggle_values[0] ? $this->toggle_values[1] : $this->toggle_values[0];
    +
    +					//increment the id
    +					$x++;
    +				}
    +
    +				//save the changes
    +				if (is_array($array) && @sizeof($array) != 0) {
    +					//save the array
    +
    +					$this->database->save($array);
    +					unset($array);
    +
    +					//set message
    +					message::add($text['message-toggle']);
    +				}
    +				unset($records, $states);
    +			}
     		}
     	}
     
     	/**
    -	 * copy rows from the database
    +	 * Copies one or more records
    +	 *
    +	 * @param array $records  An array of record IDs to delete, where each ID is an associative array
    +	 *                        containing 'uuid' and 'checked' keys. The 'checked' value indicates
    +	 *                        whether the corresponding checkbox was checked for deletion.
    +	 *
    +	 * @return void No return value; this method modifies the database state and sets a message.
     	 */
     	public function copy($records) {
    -		if (permission_exists($this->name.'_add')) {
    +		if (permission_exists($this->name . '_add')) {
     
     			//add multi-lingual support
    -				$language = new text;
    -				$text = $language->get();
    +			$language = new text;
    +			$text = $language->get();
     
     			//validate the token
    -				$token = new token;
    -				if (!$token->validate($_SERVER['PHP_SELF'])) {
    -					message::add($text['message-invalid_token'],'negative');
    -					header('Location: '.$this->location);
    -					exit;
    -				}
    +			$token = new token;
    +			if (!$token->validate($_SERVER['PHP_SELF'])) {
    +				message::add($text['message-invalid_token'], 'negative');
    +				header('Location: ' . $this->location);
    +				exit;
    +			}
     
     			//copy the checked records
    -				if (is_array($records) && @sizeof($records) != 0) {
    +			if (is_array($records) && @sizeof($records) != 0) {
     
    -					//get checked records
    -						foreach($records as $record) {
    -							if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) {
    -								$uuids[] = "'".$record['uuid']."'";
    -							}
    -						}
    +				//get checked records
    +				foreach ($records as $record) {
    +					if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) {
    +						$uuids[] = "'" . $record['uuid'] . "'";
    +					}
    +				}
     
    -					//create the array from existing data
    -						if (is_array($uuids) && @sizeof($uuids) != 0) {
    -							$sql = "select * from v_".$this->table." ";
    -							$sql .= "where ".$this->name."_uuid in (".implode(', ', $uuids).") ";
    -							$rows = $this->database->select($sql, null, 'all');
    -							if (is_array($rows) && @sizeof($rows) != 0) {
    -								$x = 0;
    -								foreach ($rows as $row) {
    -									//convert boolean values to a string
    -										foreach($row as $key => $value) {
    -											if (gettype($value) == 'boolean') {
    -												$value = $value ? 'true' : 'false';
    -												$row[$key] = $value;
    -											}
    -										}
    -
    -									//copy data
    -										$array[$this->table][$x] = $row;
    -
    -									//add copy to the description
    -										$array[$this->table][$x][$this->name.'_uuid'] = uuid();
    -										$array[$this->table][$x][$this->name.'_description'] = trim($row[$this->name.'_description']).' ('.$text['label-copy'].')';
    -
    -									//increment the id
    -										$x++;
    +				//create the array from existing data
    +				if (is_array($uuids) && @sizeof($uuids) != 0) {
    +					$sql = "select * from v_" . $this->table . " ";
    +					$sql .= "where " . $this->name . "_uuid in (" . implode(', ', $uuids) . ") ";
    +					$rows = $this->database->select($sql, null, 'all');
    +					if (is_array($rows) && @sizeof($rows) != 0) {
    +						$x = 0;
    +						foreach ($rows as $row) {
    +							//convert boolean values to a string
    +							foreach ($row as $key => $value) {
    +								if (gettype($value) == 'boolean') {
    +									$value = $value ? 'true' : 'false';
    +									$row[$key] = $value;
     								}
     							}
    -							unset($sql, $parameters, $rows, $row);
    +
    +							//copy data
    +							$array[$this->table][$x] = $row;
    +
    +							//add copy to the description
    +							$array[$this->table][$x][$this->name . '_uuid'] = uuid();
    +							$array[$this->table][$x][$this->name . '_description'] = trim($row[$this->name . '_description']) . ' (' . $text['label-copy'] . ')';
    +
    +							//increment the id
    +							$x++;
     						}
    -
    -					//save the changes and set the message
    -						if (is_array($array) && @sizeof($array) != 0) {
    -							//save the array
    -
    -								$this->database->save($array);
    -								unset($array);
    -
    -							//set message
    -								message::add($text['message-copy']);
    -						}
    -						unset($records);
    +					}
    +					unset($sql, $parameters, $rows, $row);
     				}
    +
    +				//save the changes and set the message
    +				if (is_array($array) && @sizeof($array) != 0) {
    +					//save the array
    +
    +					$this->database->save($array);
    +					unset($array);
    +
    +					//set message
    +					message::add($text['message-copy']);
    +				}
    +				unset($records);
    +			}
     		}
     	}
     
     	/**
    -	 * add default, domain and user settings to the session array
    +	 * Upgrade function to apply necessary changes and settings.
    +	 *
    +	 * @return void
    +	 */
    +	public function upgrade() {
    +
    +		//add multi-lingual support
    +		$language = new text;
    +		$text = $language->get(null, 'core/upgrade');
    +
    +		//includes files
    +		require dirname(__DIR__, 2) . "/resources/require.php";
    +
    +		//add missing default settings
    +		$this->settings();
    +
    +		//save the database object to be used by app_defaults.php
    +		$database = $this->database;
    +
    +		//get the variables
    +		$config = new config;
    +		$config_path = $config->config_file;
    +
    +		//get the list of installed apps from the core and app directories (note: GLOB_BRACE doesn't work on some systems)
    +		$config_list_1 = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php");
    +		$config_list_2 = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_menu.php");
    +		$config_list = array_merge((array)$config_list_1, (array)$config_list_2);
    +		unset($config_list_1, $config_list_2);
    +		$x = 0;
    +		foreach ($config_list as $config_path) {
    +			$app_path = dirname($config_path);
    +			$app_path = preg_replace('/\A.*(\/.*\/.*)\z/', '$1', $app_path);
    +			include($config_path);
    +			$x++;
    +		}
    +
    +		//get the domains
    +		$sql = "select * from v_domains ";
    +		$domains = $this->database->select($sql, null, 'all');
    +		unset($sql);
    +
    +		//get the list of installed apps from the core and mod directories
    +		$default_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_defaults.php");
    +
    +		//loop through all domains
    +		$domains_processed = 1;
    +		foreach ($domains as $domain) {
    +			//get the values from database and set them as php variables
    +			$domain_uuid = $domain["domain_uuid"];
    +			$domain_name = $domain["domain_name"];
    +
    +			//get the context
    +			$context = $domain_name;
    +
    +			//get the email queue settings
    +			$settings = new settings(["database" => $this->database, "domain_uuid" => $domain_uuid]);
    +
    +			//run the php code in app_defaults.php
    +			foreach ($default_list as $default_path) {
    +				include($default_path);
    +			}
    +
    +			//track the number of domains processed
    +			$domains_processed++;
    +		}
    +
    +	} //end upgrade method
    +
    +	/**
    +	 * Get the default settings for the application.
    +	 *
    +	 * This function first retrieves an array of default setting UUIDs from the database,
    +	 * then checks each app_config.php file in the project directory to see if they contain
    +	 * any default settings that are not already included in the UUID list. If so, it adds
    +	 * them to the array and attempts to insert them into the database.
    +	 *
    +	 * @return void
    +	 */
    +	public function settings() {
    +
    +		//includes files
    +		require dirname(__DIR__, 2) . "/resources/require.php";
    +
    +		//get an array of the default settings UUIDs
    +		$sql = "select * from v_default_settings ";
    +		$result = $this->database->select($sql, null, 'all');
    +		foreach ($result as $row) {
    +			$setting[$row['default_setting_uuid']] = 1;
    +		}
    +		unset($sql);
    +
    +		//get the list of default settings
    +		$config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php");
    +		$x = 0;
    +		foreach ($config_list as $config_path) {
    +			include($config_path);
    +			$x++;
    +		}
    +		$x = 0;
    +		foreach ($apps as $app) {
    +			if (isset($app['default_settings']) && is_array($app['default_settings'])) {
    +				foreach ($app['default_settings'] as $row) {
    +					if (!isset($setting[$row['default_setting_uuid']])) {
    +						$array['default_settings'][$x] = $row;
    +						$array['default_settings'][$x]['app_uuid'] = $app['uuid'];
    +						$x++;
    +					}
    +				}
    +			}
    +		}
    +
    +		//add the missing default settings
    +		if (isset($array) && is_array($array) && count($array) > 0) {
    +			//grant temporary permissions
    +			$p = permissions::new();
    +			$p->add('default_setting_add', 'temp');
    +
    +			//execute insert
    +			$this->database->app_name = 'default_settings';
    +			$this->database->app_uuid = '2c2453c0-1bea-4475-9f44-4d969650de09';
    +			$this->database->save($array, false);
    +			unset($array);
    +
    +			//revoke temporary permissions
    +			$p->delete('default_setting_add', 'temp');
    +		}
    +
    +	} //end settings method
    +
    +	/**
    +	 * Deletes one or multiple records.
    +	 *
    +	 * @param array $records An array of record IDs to delete, where each ID is an associative array
    +	 *                       containing 'uuid' and 'checked' keys. The 'checked' value indicates
    +	 *                       whether the corresponding checkbox was checked for deletion.
    +	 *
    +	 * @return void No return value; this method modifies the database state and sets a message.
    +	 */
    +	public function delete($records) {
    +		if (permission_exists($this->name . '_delete')) {
    +
    +			//add multi-lingual support
    +			$language = new text;
    +			$text = $language->get();
    +
    +			//validate the token
    +			$token = new token;
    +			if (!$token->validate($_SERVER['PHP_SELF'])) {
    +				message::add($text['message-invalid_token'], 'negative');
    +				header('Location: ' . $this->location);
    +				exit;
    +			}
    +
    +			//delete multiple records
    +			if (is_array($records) && @sizeof($records) != 0) {
    +				//build the delete array
    +				foreach ($records as $record) {
    +					//add to the array
    +					if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) {
    +						//set the uuid
    +						$id = $record['uuid'];
    +
    +						//get the domain using the id
    +						$sql = "select domain_name from v_domains ";
    +						$sql .= "where domain_uuid = :domain_uuid ";
    +						$parameters['domain_uuid'] = $id;
    +						$domain_name = $this->database->select($sql, $parameters, 'column');
    +						unset($sql, $parameters);
    +
    +						//get the domain settings
    +						$sql = "select * from v_domain_settings ";
    +						$sql .= "where domain_uuid = :domain_uuid ";
    +						$sql .= "and domain_setting_enabled = 'true' ";
    +						$parameters['domain_uuid'] = $id;
    +						$result = $this->database->select($sql, $parameters, 'all');
    +						unset($sql, $parameters);
    +
    +						if (is_array($result) && sizeof($result) != 0) {
    +							foreach ($result as $row) {
    +								$name = $row['domain_setting_name'];
    +								$category = $row['domain_setting_category'];
    +								$subcategory = $row['domain_setting_subcategory'];
    +								if ($subcategory != '') {
    +									if ($name == "array") {
    +										$_SESSION[$category][] = $row['default_setting_value'];
    +									} else {
    +										$_SESSION[$category][$name] = $row['default_setting_value'];
    +									}
    +								} else {
    +									if ($name == "array") {
    +										$_SESSION[$category][$subcategory][] = $row['default_setting_value'];
    +									} else {
    +										$_SESSION[$category][$subcategory]['uuid'] = $row['default_setting_uuid'];
    +										$_SESSION[$category][$subcategory][$name] = $row['default_setting_value'];
    +									}
    +								}
    +							}
    +						}
    +						unset($result, $row);
    +
    +						//get the $apps array from the installed apps from the core and mod directories
    +						$config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php");
    +						$x = 0;
    +						if (isset($config_list)) foreach ($config_list as $config_path) {
    +							include($config_path);
    +							$x++;
    +						}
    +
    +						//delete the domain data from all tables in the database
    +						if (isset($apps)) foreach ($apps as $app) {
    +							if (isset($app['db'])) foreach ($app['db'] as $row) {
    +								if (is_array($row['table']['name'])) {
    +									$table_name = $row['table']['name']['text'];
    +									if (defined('STDIN')) {
    +										echo "
    " . print_r($table_name, 1) . "
    \n";
    +									}
    +								} else {
    +									$table_name = $row['table']['name'];
    +								}
    +								if ($table_name !== "v" && isset($row['fields'])) {
    +									foreach ($row['fields'] as $field) {
    +										if ($field['name'] == 'domain_uuid' && $table_name != 'v_domains') {
    +											$sql = "delete from " . $table_name . " where domain_uuid = :domain_uuid ";
    +											$parameters['domain_uuid'] = $id;
    +											$this->database->app_name = 'domain_settings';
    +											$this->database->app_uuid = 'b31e723a-bf70-670c-a49b-470d2a232f71';
    +											$this->database->execute($sql, $parameters);
    +											unset($sql, $parameters);
    +										}
    +									}
    +								}
    +							}
    +						}
    +
    +						//delete the directories
    +						if (!empty($domain_name)) {
    +							//set the needle
    +							if (count($_SESSION["domains"]) > 1) {
    +								$v_needle = 'v_' . $domain_name . '_';
    +							} else {
    +								$v_needle = 'v_';
    +							}
    +
    +							//delete the dialplan
    +							@unlink($_SESSION['switch']['dialplan']['dir'] . '/' . $domain_name . '.xml');
    +							if (!empty($_SESSION['switch']['dialplan']['dir'])) {
    +								system('rm -rf ' . $_SESSION['switch']['dialplan']['dir'] . '/' . $domain_name);
    +							}
    +
    +							//delete the dialplan public
    +							@unlink($_SESSION['switch']['dialplan']['dir'] . '/public/' . $domain_name . '.xml');
    +							if (!empty($_SESSION['switch']['dialplan']['dir'])) {
    +								system('rm -rf ' . $_SESSION['switch']['dialplan']['dir'] . '/public/' . $domain_name);
    +							}
    +
    +							//delete the extension
    +							@unlink($_SESSION['switch']['extensions']['dir'] . '/' . $domain_name . '.xml');
    +							if (!empty($_SESSION['switch']['extensions']['dir'])) {
    +								system('rm -rf ' . $_SESSION['switch']['extensions']['dir'] . '/' . $domain_name);
    +							}
    +
    +							//delete fax
    +							if (!empty($_SESSION['switch']['storage']['dir'])) {
    +								system('rm -rf ' . $_SESSION['switch']['storage']['dir'] . '/fax/' . $domain_name);
    +							}
    +
    +							//delete the gateways
    +							if (!empty($_SESSION['switch']['sip_profiles']['dir'])) {
    +								if ($dh = opendir($_SESSION['switch']['sip_profiles']['dir'])) {
    +									$files = [];
    +									while ($file = readdir($dh)) {
    +										if ($file != "." && $file != ".." && $file[0] != '.') {
    +											if (is_dir($dir . "/" . $file)) {
    +												//this is a directory do nothing
    +											} else {
    +												//check if file extension is xml
    +												if (strpos($file, $v_needle) !== false && substr($file, -4) == '.xml') {
    +													@unlink($_SESSION['switch']['sip_profiles']['dir'] . "/" . $file);
    +												}
    +											}
    +										}
    +									}
    +									closedir($dh);
    +								}
    +							}
    +
    +							//delete the ivr menu
    +							if (!empty($_SESSION['switch']['conf']['dir'])) {
    +								if ($dh = opendir($_SESSION['switch']['conf']['dir'] . "/ivr_menus")) {
    +									$files = [];
    +									while ($file = readdir($dh)) {
    +										if ($file != "." && $file != ".." && $file[0] != '.') {
    +											if (!empty($dir) && !empty($file) && is_dir($dir . "/" . $file)) {
    +												//this is a directory
    +											} else {
    +												if (strpos($file, $v_needle) !== false && substr($file, -4) == '.xml') {
    +													@unlink($_SESSION['switch']['conf']['dir'] . "/ivr_menus/" . $file);
    +												}
    +											}
    +										}
    +									}
    +									closedir($dh);
    +								}
    +							}
    +
    +							//delete the recordings
    +							if (!empty($_SESSION['switch']['recordings']['dir'])) {
    +								system('rm -rf ' . $_SESSION['switch']['recordings']['dir'] . '/' . $_SESSION['domain_name'] . '/' . $domain_name);
    +							}
    +
    +							//delete voicemail
    +							if (!empty($_SESSION['switch']['voicemail']['dir'])) {
    +								system('rm -rf ' . $_SESSION['switch']['voicemail']['dir'] . '/' . $domain_name);
    +							}
    +						}
    +
    +						//apply settings reminder
    +						$_SESSION["reload_xml"] = true;
    +
    +						//remove the domain from domains session array
    +						unset($_SESSION["domains"][$id]);
    +
    +						//add domain uuid to array for deletion below
    +						$domain_array['domains'][] = ['domain_uuid' => $id];
    +					}
    +				}
    +
    +				//delete the checked rows
    +				if (is_array($domain_array) && @sizeof($domain_array) != 0) {
    +					//execute delete
    +					$this->database->delete($domain_array);
    +
    +					//set message
    +					message::add($text['message-delete']);
    +
    +					//reload default/domain settings
    +					$this->set();
    +				}
    +				unset($records);
    +			}
    +		}
    +	}
    +
    +	/**
    +	 * set domain settings
    +	 *
    +	 * This method retrieves domain and user settings from the database,
    +	 * unsets previous domain settings, sets default settings as session variables,
    +	 * and updates the time zone based on the domain's time zone setting.
    +	 *
    +	 * @return void
     	 */
     	public function set() {
     
     		//get previous domain settings
    -			if (isset($_SESSION["previous_domain_uuid"])) {
    -				$sql = "select * from v_domain_settings ";
    -				$sql .= "where domain_uuid = :previous_domain_uuid ";
    -				$sql .= "and domain_setting_enabled = 'true' ";
    -				$sql .= " order by domain_setting_order asc ";
    -				$parameters['previous_domain_uuid'] = $_SESSION["previous_domain_uuid"];
    -				$result = $this->database->select($sql, $parameters, 'all');
    -				unset($sql, $parameters);
    -
    -				//unset previous domain settings
    -				foreach ($result as $row) {
    -					if ($row['domain_setting_category'] != 'user') { //skip off-limit categories
    -						unset($_SESSION[$row['domain_setting_category']][$row['domain_setting_subcategory']]);
    -					}
    -				}
    -				unset($_SESSION["previous_domain_uuid"]);
    -			}
    -
    -		//get the default settings
    -			$sql = "select * from v_default_settings ";
    -			$sql .= "order by default_setting_order asc ";
    -			$result = $this->database->select($sql, null, 'all');
    +		if (isset($_SESSION["previous_domain_uuid"])) {
    +			$sql = "select * from v_domain_settings ";
    +			$sql .= "where domain_uuid = :previous_domain_uuid ";
    +			$sql .= "and domain_setting_enabled = 'true' ";
    +			$sql .= " order by domain_setting_order asc ";
    +			$parameters['previous_domain_uuid'] = $_SESSION["previous_domain_uuid"];
    +			$result = $this->database->select($sql, $parameters, 'all');
     			unset($sql, $parameters);
     
    -			//unset all settings
    +			//unset previous domain settings
     			foreach ($result as $row) {
    -				if ($row['default_setting_category'] != 'user') { //skip off-limit categories
    -					unset($_SESSION[$row['default_setting_category']][$row['default_setting_subcategory']]);
    +				if ($row['domain_setting_category'] != 'user') { //skip off-limit categories
    +					unset($_SESSION[$row['domain_setting_category']][$row['domain_setting_subcategory']]);
    +				}
    +			}
    +			unset($_SESSION["previous_domain_uuid"]);
    +		}
    +
    +		//get the default settings
    +		$sql = "select * from v_default_settings ";
    +		$sql .= "order by default_setting_order asc ";
    +		$result = $this->database->select($sql, null, 'all');
    +		unset($sql, $parameters);
    +
    +		//unset all settings
    +		foreach ($result as $row) {
    +			if ($row['default_setting_category'] != 'user') { //skip off-limit categories
    +				unset($_SESSION[$row['default_setting_category']][$row['default_setting_subcategory']]);
    +			}
    +		}
    +
    +		//set the enabled settings as a session
    +		foreach ($result as $row) {
    +			if ($row['default_setting_enabled'] == 'true') {
    +				$name = $row['default_setting_name'];
    +				$category = $row['default_setting_category'];
    +				$subcategory = $row['default_setting_subcategory'];
    +				if (empty($subcategory)) {
    +					if ($name == "array") {
    +						$_SESSION[$category][] = $row['default_setting_value'];
    +					} else {
    +						$_SESSION[$category][$name] = $row['default_setting_value'];
    +					}
    +				} else {
    +					if ($name == "array") {
    +						$_SESSION[$category][$subcategory][] = $row['default_setting_value'];
    +					} else {
    +						$_SESSION[$category][$subcategory]['uuid'] = $row['default_setting_uuid'];
    +						$_SESSION[$category][$subcategory][$name] = $row['default_setting_value'];
    +					}
    +				}
    +			}
    +		}
    +
    +		//get the domains settings
    +		if (!empty($_SESSION["domain_uuid"]) && is_uuid($_SESSION["domain_uuid"])) {
    +
    +			//get settings from the database
    +			$sql = "select * from v_domain_settings ";
    +			$sql .= "where domain_uuid = :domain_uuid ";
    +			$sql .= "and domain_setting_enabled = 'true' ";
    +			$sql .= " order by domain_setting_order asc ";
    +			$parameters['domain_uuid'] = $_SESSION["domain_uuid"];
    +			$result = $this->database->select($sql, $parameters, 'all');
    +			unset($sql, $parameters);
    +
    +			//unset the arrays that domains are overriding
    +			foreach ($result as $row) {
    +				$name = $row['domain_setting_name'];
    +				$category = $row['domain_setting_category'];
    +				$subcategory = $row['domain_setting_subcategory'];
    +				if ($name == "array") {
    +					unset($_SESSION[$category][$subcategory]);
     				}
     			}
     
     			//set the enabled settings as a session
     			foreach ($result as $row) {
    -				if ($row['default_setting_enabled'] == 'true') {
    -					$name = $row['default_setting_name'];
    -					$category = $row['default_setting_category'];
    -					$subcategory = $row['default_setting_subcategory'];
    -					if (empty($subcategory)) {
    -						if ($name == "array") {
    -							$_SESSION[$category][] = $row['default_setting_value'];
    -						}
    -						else {
    -							$_SESSION[$category][$name] = $row['default_setting_value'];
    -						}
    -					}
    -					else {
    -						if ($name == "array") {
    -							$_SESSION[$category][$subcategory][] = $row['default_setting_value'];
    -						}
    -						else {
    -							$_SESSION[$category][$subcategory]['uuid'] = $row['default_setting_uuid'];
    -							$_SESSION[$category][$subcategory][$name] = $row['default_setting_value'];
    -						}
    -					}
    -				}
    -			}
    -
    -		//get the domains settings
    -			if (!empty($_SESSION["domain_uuid"]) && is_uuid($_SESSION["domain_uuid"])) {
    -
    -				//get settings from the database
    -				$sql = "select * from v_domain_settings ";
    -				$sql .= "where domain_uuid = :domain_uuid ";
    -				$sql .= "and domain_setting_enabled = 'true' ";
    -				$sql .= " order by domain_setting_order asc ";
    -				$parameters['domain_uuid'] = $_SESSION["domain_uuid"];
    -				$result = $this->database->select($sql, $parameters, 'all');
    -				unset($sql, $parameters);
    -
    -				//unset the arrays that domains are overriding
    -				foreach ($result as $row) {
    +				if ($row['domain_setting_enabled'] == 'true') {
     					$name = $row['domain_setting_name'];
     					$category = $row['domain_setting_category'];
     					$subcategory = $row['domain_setting_subcategory'];
    -					if ($name == "array") {
    -						unset($_SESSION[$category][$subcategory]);
    -					}
    -				}
    -
    -				//set the enabled settings as a session
    -				foreach ($result as $row) {
    -					if ($row['domain_setting_enabled'] == 'true') {
    -						$name = $row['domain_setting_name'];
    -						$category = $row['domain_setting_category'];
    -						$subcategory = $row['domain_setting_subcategory'];
    -						if (empty($subcategory)) {
    -							//$$category[$name] = $row['domain_setting_value'];
    -							if ($name == "array") {
    -								$_SESSION[$category][] = $row['domain_setting_value'];
    -							}
    -							else {
    -								$_SESSION[$category][$name] = $row['domain_setting_value'];
    -							}
    +					if (empty($subcategory)) {
    +						//$$category[$name] = $row['domain_setting_value'];
    +						if ($name == "array") {
    +							$_SESSION[$category][] = $row['domain_setting_value'];
    +						} else {
    +							$_SESSION[$category][$name] = $row['domain_setting_value'];
     						}
    -						else {
    -							//$$category[$subcategory][$name] = $row['domain_setting_value'];
    -							if ($name == "array") {
    -								$_SESSION[$category][$subcategory][] = $row['domain_setting_value'];
    -							}
    -							else {
    -								$_SESSION[$category][$subcategory][$name] = $row['domain_setting_value'];
    -							}
    +					} else {
    +						//$$category[$subcategory][$name] = $row['domain_setting_value'];
    +						if ($name == "array") {
    +							$_SESSION[$category][$subcategory][] = $row['domain_setting_value'];
    +						} else {
    +							$_SESSION[$category][$subcategory][$name] = $row['domain_setting_value'];
     						}
     					}
     				}
     			}
    +		}
     
     		//get the user settings
    -			if (array_key_exists("domain_uuid",$_SESSION) && array_key_exists("user_uuid",$_SESSION) && is_uuid($_SESSION["domain_uuid"])) {
    -				$sql = "select * from v_user_settings ";
    -				$sql .= "where domain_uuid = :domain_uuid ";
    -				$sql .= "and user_uuid = :user_uuid ";
    -				$sql .= " order by user_setting_order asc ";
    -				$parameters['domain_uuid'] = $_SESSION["domain_uuid"];
    -				$parameters['user_uuid'] = $_SESSION["user_uuid"];
    -				$result = $this->database->select($sql, $parameters, 'all');
    -				if (is_array($result)) {
    -					foreach ($result as $row) {
    -						if ($row['user_setting_enabled'] == 'true') {
    -							$name = $row['user_setting_name'];
    -							$category = $row['user_setting_category'];
    -							$subcategory = $row['user_setting_subcategory'];
    -							if (!empty($row['user_setting_value'])) {
    -								if (empty($subcategory)) {
    -									//$$category[$name] = $row['domain_setting_value'];
    -									if ($name == "array") {
    -										$_SESSION[$category][] = $row['user_setting_value'];
    -									}
    -									else {
    -										$_SESSION[$category][$name] = $row['user_setting_value'];
    -									}
    +		if (array_key_exists("domain_uuid", $_SESSION) && array_key_exists("user_uuid", $_SESSION) && is_uuid($_SESSION["domain_uuid"])) {
    +			$sql = "select * from v_user_settings ";
    +			$sql .= "where domain_uuid = :domain_uuid ";
    +			$sql .= "and user_uuid = :user_uuid ";
    +			$sql .= " order by user_setting_order asc ";
    +			$parameters['domain_uuid'] = $_SESSION["domain_uuid"];
    +			$parameters['user_uuid'] = $_SESSION["user_uuid"];
    +			$result = $this->database->select($sql, $parameters, 'all');
    +			if (is_array($result)) {
    +				foreach ($result as $row) {
    +					if ($row['user_setting_enabled'] == 'true') {
    +						$name = $row['user_setting_name'];
    +						$category = $row['user_setting_category'];
    +						$subcategory = $row['user_setting_subcategory'];
    +						if (!empty($row['user_setting_value'])) {
    +							if (empty($subcategory)) {
    +								//$$category[$name] = $row['domain_setting_value'];
    +								if ($name == "array") {
    +									$_SESSION[$category][] = $row['user_setting_value'];
    +								} else {
    +									$_SESSION[$category][$name] = $row['user_setting_value'];
     								}
    -								else {
    -									//$$category[$subcategory][$name] = $row['domain_setting_value'];
    -									if ($name == "array") {
    -										$_SESSION[$category][$subcategory][] = $row['user_setting_value'];
    -									}
    -									else {
    -										$_SESSION[$category][$subcategory][$name] = $row['user_setting_value'];
    -									}
    +							} else {
    +								//$$category[$subcategory][$name] = $row['domain_setting_value'];
    +								if ($name == "array") {
    +									$_SESSION[$category][$subcategory][] = $row['user_setting_value'];
    +								} else {
    +									$_SESSION[$category][$subcategory][$name] = $row['user_setting_value'];
     								}
     							}
     						}
     					}
     				}
     			}
    +		}
     
     		//set the domain time zone as the default time zone
    -			date_default_timezone_set($this->settings->get('domain', 'time_zone', date_default_timezone_get()));
    +		date_default_timezone_set($this->settings->get('domain', 'time_zone', date_default_timezone_get()));
     
     		//set the context
    -			if (!empty($_SESSION["domain_name"])) {
    -				$_SESSION["context"] = $_SESSION["domain_name"];
    -			}
    +		if (!empty($_SESSION["domain_name"])) {
    +			$_SESSION["context"] = $_SESSION["domain_name"];
    +		}
     	}
     
     	/**
    -	 * upgrade application defaults
    +	 * Initializes a session with domain-specific data.
    +	 *
    +	 * This method retrieves a list of all domains from the database and then determines the current domain by checking
    +	 * the HTTP_HOST server variable. If the username is not set in the session, it will use the first domain in the
    +	 * list or match the domain name with the given domain name. The relevant domain UUID and name are then stored in
    +	 * the session.
    +	 *
    +	 * @return void
     	 */
    -	public function upgrade() {
    +	public function session() {
    +		//get the list of domains
    +		$domains = self::all();
     
    -		//add multi-lingual support
    -			$language = new text;
    -			$text = $language->get(null, 'core/upgrade');
    +		//get the domain
    +		$domain_array = explode(":", $_SERVER["HTTP_HOST"] ?? '');
     
    -		//includes files
    -			require dirname(__DIR__, 2) . "/resources/require.php";
    -
    -		//add missing default settings
    -			$this->settings();
    -
    -		//save the database object to be used by app_defaults.php
    -			$database = $this->database;
    -
    -		//get the variables
    -			$config = new config;
    -			$config_path = $config->config_file;
    -
    -		//get the list of installed apps from the core and app directories (note: GLOB_BRACE doesn't work on some systems)
    -			$config_list_1 = glob($_SERVER["DOCUMENT_ROOT"].PROJECT_PATH."/*/*/app_config.php");
    -			$config_list_2 = glob($_SERVER["DOCUMENT_ROOT"].PROJECT_PATH."/*/*/app_menu.php");
    -			$config_list = array_merge((array)$config_list_1, (array)$config_list_2);
    -			unset($config_list_1,$config_list_2);
    -			$x=0;
    -			foreach ($config_list as $config_path) {
    -				$app_path = dirname($config_path);
    -				$app_path = preg_replace('/\A.*(\/.*\/.*)\z/', '$1', $app_path);
    -				include($config_path);
    -				$x++;
    -			}
    -
    -		//get the domains
    -			$sql = "select * from v_domains ";
    -			$domains = $this->database->select($sql, null, 'all');
    -			unset($sql);
    -
    -		//get the list of installed apps from the core and mod directories
    -			$default_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_defaults.php");
    -
    -		//loop through all domains
    -			$domains_processed = 1;
    -			foreach ($domains as $domain) {
    -				//get the values from database and set them as php variables
    -					$domain_uuid = $domain["domain_uuid"];
    -					$domain_name = $domain["domain_name"];
    -
    -				//get the context
    -					$context = $domain_name;
    -
    -				//get the email queue settings
    -					$settings = new settings(["database" => $this->database, "domain_uuid" => $domain_uuid]);
    -
    -				//run the php code in app_defaults.php
    -					foreach ($default_list as $default_path) {
    -						include($default_path);
    -					}
    -
    -				//track the number of domains processed
    -					$domains_processed++;
    -			}
    -
    -	} //end upgrade method
    -
    -	/**
    -	 * add missing default settings
    -	 * update the uuid for older default settings that were added before the uuids was predefined.
    -	 */
    -	public function settings() {
    -
    -		//includes files
    -			require dirname(__DIR__, 2) . "/resources/require.php";
    -
    -		//get an array of the default settings UUIDs
    -			$sql = "select * from v_default_settings ";
    -			$result = $this->database->select($sql, null, 'all');
    -			foreach($result as $row) {
    -				$setting[$row['default_setting_uuid']] = 1;
    -			}
    -			unset($sql);
    -
    -		//get the list of default settings
    -			$config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php");
    -			$x=0;
    -			foreach ($config_list as $config_path) {
    -				include($config_path);
    -				$x++;
    -			}
    -			$x = 0;
    -			foreach ($apps as $app) {
    -				if (isset($app['default_settings']) && is_array($app['default_settings'])) {
    -					foreach ($app['default_settings'] as $row) {
    -						if (!isset($setting[$row['default_setting_uuid']])) {
    -							$array['default_settings'][$x] = $row;
    -							$array['default_settings'][$x]['app_uuid'] = $app['uuid'];
    -							$x++;
    -						}
    +		//set domain_name and domain_uuid and update domains array with domain_uuid as the key
    +		foreach ($domains as $row) {
    +			if (!isset($_SESSION['username'])) {
    +				if (!empty($domains) && count($domains) == 1) {
    +					$_SESSION["domain_uuid"] = $row["domain_uuid"];
    +					$_SESSION["domain_name"] = $row['domain_name'];
    +				} else {
    +					if ($row['domain_name'] == $domain_array[0] || $row['domain_name'] == 'www.' . $domain_array[0]) {
    +						$_SESSION["domain_uuid"] = $row["domain_uuid"];
    +						$_SESSION["domain_name"] = $row["domain_name"];
     					}
     				}
     			}
    +		}
     
    -		//add the missing default settings
    -			if (isset($array) && is_array($array) && count($array) > 0) {
    -				//grant temporary permissions
    -					$p = permissions::new();
    -					$p->add('default_setting_add', 'temp');
    -
    -				//execute insert
    -					$this->database->app_name = 'default_settings';
    -					$this->database->app_uuid = '2c2453c0-1bea-4475-9f44-4d969650de09';
    -					$this->database->save($array, false);
    -					unset($array);
    -
    -				//revoke temporary permissions
    -					$p->delete('default_setting_add', 'temp');
    -			}
    -
    -	} //end settings method
    -
    -	/**
    -	 * get all enabled domains
    -	 * @returns array enabled domains with uuid as array key
    -	 */
    -	public static function enabled() {
    -
    -		//define database as global
    -			global $database;
    -
    -		//define default return value
    -			$domains = [];
    -
    -		//get the domains from the database
    -			$sql = "select * from v_domains ";
    -			$sql .= "where domain_enabled = true ";
    -			$sql .= "order by domain_name asc; ";
    -			$result = $database->select($sql, null, 'all');
    -			if (!empty($result)) {
    -				foreach($result as $row) {
    -					$domains[$row['domain_uuid']] = $row;
    -				}
    -			}
    -
    -		//return the domains array
    -			return $domains;
    +		//set the domains session array
    +		$_SESSION['domains'] = $domains;
     	}
     
     	/**
    -	 * get all disabled domains
    -	 * @returns array disabled domains with uuid as array key
    -	 */
    -	public static function disabled() {
    -
    -		//define database as global
    -			global $database;
    -
    -		//define default return value
    -			$domains = [];
    -
    -		//get the domains from the database
    -			$sql = "select * from v_domains ";
    -			$sql .= "where domain_enabled = false ";
    -			$sql .= "order by domain_name asc; ";
    -			$result = $database->select($sql, null, 'all');
    -			if (!empty($result)) {
    -				foreach($result as $row) {
    -					$domains[$row['domain_uuid']] = $row;
    -				}
    -			}
    -
    -		//return the domains array
    -			return $domains;
    -	}
    -
    -	/**
    -	 * get all domains
    -	 * @returns array all domains with uuid as array key
    +	 * Retrieves a list of all domains from the database.
    +	 *
    +	 * @return array An array of domain data, where each key is a unique domain UUID and each value is an associative
    +	 *               array containing the domain's details.
     	 */
     	public static function all() {
     
     		//define database as global
    -			global $database;
    +		global $database;
     
     		//define default return value
    -			$domains = [];
    +		$domains = [];
     
     		//get the domains from the database
    -			$sql = "select * from v_domains ";
    -			$sql .= "order by domain_name asc; ";
    -			$result = $database->select($sql, null, 'all');
    -			if (!empty($result)) {
    -				foreach($result as $row) {
    -					$domains[$row['domain_uuid']] = $row;
    -				}
    +		$sql = "select * from v_domains ";
    +		$sql .= "order by domain_name asc; ";
    +		$result = $database->select($sql, null, 'all');
    +		if (!empty($result)) {
    +			foreach ($result as $row) {
    +				$domains[$row['domain_uuid']] = $row;
     			}
    +		}
     
     		//return the domains array
    -			return $domains;
    -	}
    -
    -	/**
    -	 * get a domain list
    -	 * 	@returns void
    -	 */
    -	public function session() {
    -		//get the list of domains
    -			$domains = self::all();
    -
    -		//get the domain
    -			$domain_array = explode(":", $_SERVER["HTTP_HOST"] ?? '');
    -
    -		//set domain_name and domain_uuid and update domains array with domain_uuid as the key
    -			foreach($domains as $row) {
    -				if (!isset($_SESSION['username'])) {
    -					if (!empty($domains) && count($domains) == 1) {
    -						$_SESSION["domain_uuid"] = $row["domain_uuid"];
    -						$_SESSION["domain_name"] = $row['domain_name'];
    -					}
    -					else {
    -						if ($row['domain_name'] == $domain_array[0] || $row['domain_name'] == 'www.'.$domain_array[0]) {
    -							$_SESSION["domain_uuid"] = $row["domain_uuid"];
    -							$_SESSION["domain_name"] = $row["domain_name"];
    -						}
    -					}
    -				}
    -			}
    -
    -		//set the domains session array
    -			$_SESSION['domains'] = $domains;
    +		return $domains;
     	}
     
     }
    diff --git a/resources/classes/email.php b/resources/classes/email.php
    index 008d257d9d..278c0eea4b 100644
    --- a/resources/classes/email.php
    +++ b/resources/classes/email.php
    @@ -28,613 +28,627 @@
      * email class
      *
      */
    -	class email {
    +class email {
     
    -		/**
    -		 * declare constant variables
    -		 */
    -		const app_name = 'email';
    -		const app_uuid = '7a4fef67-5bf8-436a-ae25-7e3c03afcf96';
    +	/**
    +	 * declare constant variables
    +	 */
    +	const app_name = 'email';
    +	const app_uuid = '7a4fef67-5bf8-436a-ae25-7e3c03afcf96';
     
    -		/**
    -		 * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array
    -		 * @var string
    -		 */
    -		public $domain_uuid;
    +	/**
    +	 * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set
    +	 * in the session global array
    +	 *
    +	 * @var string
    +	 */
    +	public $domain_uuid;
     
    -		/**
    -		* declare public variables
    -		*/
    -		public $method;
    -		public $recipients;
    -		public $subject;
    -		public $body;
    -		public $from_address;
    -		public $from_name;
    -		public $priority;
    -		public $debug_level;
    -		public $attachments;
    -		public $read_confirmation;
    -		public $error;
    -		public $response;
    -		public $headers;
    -		public $content_type;
    -		public $reply_to;
    -		public $date;
    +	/**
    +	 * declare public variables
    +	 */
    +	public $method;
    +	public $recipients;
    +	public $subject;
    +	public $body;
    +	public $from_address;
    +	public $from_name;
    +	public $priority;
    +	public $debug_level;
    +	public $attachments;
    +	public $read_confirmation;
    +	public $error;
    +	public $response;
    +	public $headers;
    +	public $content_type;
    +	public $reply_to;
    +	public $date;
     
    -		/**
    -		 * Set in the constructor. Must be a database object and cannot be null.
    -		 * @var database Database Object
    -		 */
    -		private $database;
    +	/**
    +	 * Set in the constructor. Must be a database object and cannot be null.
    +	 *
    +	 * @var database Database Object
    +	 */
    +	private $database;
     
    -		/**
    -		 * Settings object set in the constructor. Must be a settings object and cannot be null.
    -		 * @var settings Settings Object
    -		 */
    -		private $settings;
    +	/**
    +	 * Settings object set in the constructor. Must be a settings object and cannot be null.
    +	 *
    +	 * @var settings Settings Object
    +	 */
    +	private $settings;
     
    -		/**
    -		 * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array
    -		 * @var string
    -		 */
    -		private $user_uuid;
    +	/**
    +	 * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in
    +	 * the session global array
    +	 *
    +	 * @var string
    +	 */
    +	private $user_uuid;
     
    -		/**
    -		* declare private variables
    -		*/
    -		private $name;
    +	/**
    +	 * declare private variables
    +	 *
    +	 * @var string $name Property is only written but never read
    +	 */
    +	private $name;
     
    -		/**
    -		 * called when the object is created
    -		 */
    -		public function __construct(array $setting_array = []) {
    -			//assign the variables
    -			$this->name = 'email';
    -			$this->priority = 0;
    -			$this->debug_level = 3;
    -			$this->read_confirmation = false;
    +	/**
    +	 * called when the object is created
    +	 */
    +	public function __construct(array $setting_array = []) {
    +		//assign the variables
    +		$this->name = 'email'; //unused
    +		$this->priority = 0;
    +		$this->debug_level = 3;
    +		$this->read_confirmation = false;
     
    -			//set the domain and user uuids
    -			$this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? '';
    -			$this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? '';
    +		//set the domain and user uuids
    +		$this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? '';
    +		$this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? '';
     
    -			//set the objects
    -			$this->database = $setting_array['database'] ?? database::new();
    -			$this->settings = $setting_array['settings'] ?? new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]);
    +		//set the objects
    +		$this->database = $setting_array['database'] ?? database::new();
    +		$this->settings = $setting_array['settings'] ?? new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]);
    +	}
    +
    +	/**
    +	 * Parse an email message.
    +	 *
    +	 * This method takes a raw email message as input and parses it into its constituent parts,
    +	 * including headers, body, attachments, and other metadata. The parsed data is stored in
    +	 * various properties of the object, making it available for further processing or storage.
    +	 *
    +	 * @param string $message Raw email message to be parsed.
    +	 */
    +	public function parse($message) {
    +		//includes
    +		require_once('resources/pop3/mime_parser.php');
    +		require_once('resources/pop3/rfc822_addresses.php');
    +		if (file_exists($_SERVER["PROJECT_ROOT"] . "/app/emails/email_transcription.php")) {
    +			require_once($_SERVER["PROJECT_ROOT"] . "/app/emails/email_transcription.php");
     		}
     
    -		/**
    -		 * parse raw emails
    -		 */
    -		public function parse($message) {
    -			//includes
    -			require_once('resources/pop3/mime_parser.php');
    -			require_once('resources/pop3/rfc822_addresses.php');
    -			if (file_exists($_SERVER["PROJECT_ROOT"]."/app/emails/email_transcription.php")) {
    -				require_once($_SERVER["PROJECT_ROOT"]."/app/emails/email_transcription.php");
    -			}
    +		//parse the email message
    +		$mime = new mime_parser_class;
    +		$mime->decode_bodies = 1;
    +		$parameters = [
    +			//'File'=>$message_file,
     
    -			//parse the email message
    -			$mime = new mime_parser_class;
    -			$mime->decode_bodies = 1;
    -			$parameters = array(
    -				//'File'=>$message_file,
    +			// Read a message from a string instead of a file
    +			'Data' => $message,
     
    -				// Read a message from a string instead of a file
    -				'Data' => $message,
    +			// Save the message body parts to a directory
    +			// 'SaveBody' => '/tmp',
     
    -				// Save the message body parts to a directory
    -				// 'SaveBody' => '/tmp',
    +			// Do not retrieve or save message body parts
    +			//   'SkipBody' => 1,
    +		];
    +		$success = $mime->Decode($parameters, $decoded);
    +		unset($parameters);
     
    -				// Do not retrieve or save message body parts
    -				//   'SkipBody' => 1,
    -			);
    -			$success = $mime->Decode($parameters, $decoded);
    -			unset($parameters);
    +		if (!$success) {
    +			echo "MIME message decoding error: " . HtmlSpecialChars($mime->error) . "\n";
    +		} else {
     
    -			if (!$success) {
    -				echo "MIME message decoding error: ".HtmlSpecialChars($mime->error)."\n";
    -			}
    -			else {
    +			//get the headers
    +			$this->headers = json_decode($decoded[0]["Headers"]["x-headers:"], true);
    +			$this->subject = $decoded[0]["Headers"]["subject:"];
    +			$this->from_name = $decoded[0]["ExtractedAddresses"]["from:"][0]["name"];
    +			$this->from_address = $decoded[0]["ExtractedAddresses"]["from:"][0]["address"];
    +			$this->reply_to = $decoded[0]["Headers"]["reply-to:"];
    +			$this->recipients = $decoded[0]["ExtractedAddresses"]["to:"];
    +			$this->date = $decoded[0]["Headers"]["date:"];
     
    -				//get the headers
    -				$this->headers = json_decode($decoded[0]["Headers"]["x-headers:"], true);
    -				$this->subject = $decoded[0]["Headers"]["subject:"];
    -				$this->from_name = $decoded[0]["ExtractedAddresses"]["from:"][0]["name"];
    -				$this->from_address = $decoded[0]["ExtractedAddresses"]["from:"][0]["address"];
    -				$this->reply_to = $decoded[0]["Headers"]["reply-to:"];
    -				$this->recipients = $decoded[0]["ExtractedAddresses"]["to:"];
    -				$this->date = $decoded[0]["Headers"]["date:"];
    +			//debug information
    +			//view_array($decoded[0]);
    +			//view_array($this);
    +			//view_array($this->recipients);
     
    -				//debug information
    -				//view_array($decoded[0]);
    -				//view_array($this);
    -				//view_array($this->recipients);
    +			//get the body
    +			$this->body = ''; //$parts_array["Parts"][0]["Headers"]["content-type:"];
     
    -				//get the body
    -				$this->body = ''; //$parts_array["Parts"][0]["Headers"]["content-type:"];
    -
    -				//get the body
    -				$this->body = '';
    -				$this->content_type = $decoded[0]['Headers']['content-type:'];
    -				if (substr($this->content_type, 0, 15) == "multipart/mixed" || substr($this->content_type, 0, 21) == "multipart/alternative") {
    -					foreach ($decoded[0]["Parts"] as $row) {
    -						$body_content_type = $row["Headers"]["content-type:"];
    -						if (substr($body_content_type, 0, 9) == "text/html") {
    -							$this->body = $row["Body"];
    -						}
    -						if (substr($body_content_type, 0, 10) == "text/plain") {
    -							$body_plain = $row["Body"];
    -							$this->body = $body_plain;
    -						}
    +			//get the body
    +			$this->body = '';
    +			$this->content_type = $decoded[0]['Headers']['content-type:'];
    +			if (substr($this->content_type, 0, 15) == "multipart/mixed" || substr($this->content_type, 0, 21) == "multipart/alternative") {
    +				foreach ($decoded[0]["Parts"] as $row) {
    +					$body_content_type = $row["Headers"]["content-type:"];
    +					if (substr($body_content_type, 0, 9) == "text/html") {
    +						$this->body = $row["Body"];
    +					}
    +					if (substr($body_content_type, 0, 10) == "text/plain") {
    +						$body_plain = $row["Body"];
    +						$this->body = $body_plain;
     					}
     				}
    -				else {
    -					$content_type_array = explode(";", $content_type);
    -					$this->body = $decoded[0]["Body"];
    -					//if ($content_type_array[0] == "text/html" || $content_type_array[0] == "text/plain") {
    -					//	$body = $row["Body"];
    -					//}
    -				}
    +			} else {
    +				$content_type_array = explode(";", $content_type);
    +				$this->body = $decoded[0]["Body"];
    +				//if ($content_type_array[0] == "text/html" || $content_type_array[0] == "text/plain") {
    +				//	$body = $row["Body"];
    +				//}
    +			}
     
    -				//get the attachments and add to the email
    -				$x = 0;
    -				foreach ($decoded[0]["Parts"] as $parts_array) {
    -					//image/tiff;name="testfax.tif"
    -					//text/plain; charset=ISO-8859-1; format=flowed
    -					$content_type = $parts_array["Parts"][0]["Headers"]["content-type:"];
    +			//get the attachments and add to the email
    +			$x = 0;
    +			foreach ($decoded[0]["Parts"] as $parts_array) {
    +				//image/tiff;name="testfax.tif"
    +				//text/plain; charset=ISO-8859-1; format=flowed
    +				$content_type = $parts_array["Parts"][0]["Headers"]["content-type:"];
     
    -					//base64, 7bit
    -					$content_transfer_encoding = $parts_array["Parts"][0]["Headers"]["content-transfer-encoding:"];
    +				//base64, 7bit
    +				$content_transfer_encoding = $parts_array["Parts"][0]["Headers"]["content-transfer-encoding:"];
     
    -					//inline;filename="testfax.tif"
    -					$content_disposition = $parts_array["Parts"][0]["Headers"]["content-disposition"];
    +				//inline;filename="testfax.tif"
    +				$content_disposition = $parts_array["Parts"][0]["Headers"]["content-disposition"];
     
    -					//testfax.tif
    -					$file = $parts_array["FileName"];
    +				//testfax.tif
    +				$file = $parts_array["FileName"];
     
    -					//inline
    -					$filedisposition = $parts_array["FileDisposition"];
    +				//inline
    +				$filedisposition = $parts_array["FileDisposition"];
     
    -					$body_part = $parts_array["BodyPart"];
    -					$body_length = $parts_array["BodyLength"];
    +				$body_part = $parts_array["BodyPart"];
    +				$body_length = $parts_array["BodyLength"];
     
    -					if (!empty($file)) {
    -						//get the file information
    -							$file_ext = pathinfo($file, PATHINFO_EXTENSION);
    -							$file_name = substr($file, 0, (strlen($file) - strlen($file_ext))-1 );
    -							$encoding = "base64"; //base64_decode
    +				if (!empty($file)) {
    +					//get the file information
    +					$file_ext = pathinfo($file, PATHINFO_EXTENSION);
    +					$file_name = substr($file, 0, (strlen($file) - strlen($file_ext)) - 1);
    +					$encoding = "base64"; //base64_decode
     
    -							switch ($file_ext){
    -								case "wav":
    -									$mime_type = "audio/x-wav";
    -									break;
    -								case "mp3":
    -									$mime_type = "audio/x-mp3";
    -									break;
    -								case "pdf":
    -									$mime_type = "application/pdf";
    -									break;
    -								case "tif":
    -									$mime_type = "image/tiff";
    -									break;
    -								case "tiff":
    -									$mime_type = "image/tiff";
    -									break;
    -								default:
    -									$mime_type = "binary/octet-stream";
    -									break;
    -							}
    -
    -						//add attachment(s)
    -							$this->attachments[$x]['type'] = 'string';
    -							$this->attachments[$x]['name'] = $file;
    -							$this->attachments[$x]['value'] = $parts_array["Body"];
    -
    -						//increment the id
    -							$x++;
    +					switch ($file_ext) {
    +						case "wav":
    +							$mime_type = "audio/x-wav";
    +							break;
    +						case "mp3":
    +							$mime_type = "audio/x-mp3";
    +							break;
    +						case "pdf":
    +							$mime_type = "application/pdf";
    +							break;
    +						case "tif":
    +							$mime_type = "image/tiff";
    +							break;
    +						case "tiff":
    +							$mime_type = "image/tiff";
    +							break;
    +						default:
    +							$mime_type = "binary/octet-stream";
    +							break;
     					}
    -				}
     
    +					//add attachment(s)
    +					$this->attachments[$x]['type'] = 'string';
    +					$this->attachments[$x]['name'] = $file;
    +					$this->attachments[$x]['value'] = $parts_array["Body"];
    +
    +					//increment the id
    +					$x++;
    +				}
    +			}
    +
    +		}
    +	}
    +
    +	/**
    +	 * Sends an email.
    +	 *
    +	 * This method determines whether to use a queue or send directly based on email_queue.enabled setting.
    +	 * If using a queue, it prepares and adds the email to the queue. If sending directly,
    +	 * it uses PHPMailer to send the email.
    +	 *
    +	 * @return string A human-readable response for debugging.
    +	 */
    +	public function send() {
    +
    +		//set the send_method if not already set
    +		if (!isset($this->method)) {
    +			if ($this->settings->get('email_queue', 'enabled', true)) {
    +				$this->method = 'queue';
    +			} else {
    +				$this->method = 'direct';
     			}
     		}
     
    -		/**
    -		 * send emails
    -		 */
    -		public function send() {
    +		//add the email to the queue
    +		if ($this->method == 'queue') {
     
    -			//set the send_method if not already set
    -			if (!isset($this->method)) {
    -				if ($this->settings->get('email_queue','enabled', true)) {
    -					$this->method = 'queue';
    -				}
    -				else {
    -					$this->method = 'direct';
    +			//add the email_queue_uuid
    +			$email_queue_uuid = uuid();
    +
    +			//set the email from address and name
    +			$email_from = $this->from_address;
    +			if (!empty($this->from_name)) {
    +				$email_from = $this->from_name . '<' . $email_from . '>';
    +			}
    +
    +			//prepare the array
    +			$array['email_queue'][0]['email_queue_uuid'] = $email_queue_uuid;
    +			$array['email_queue'][0]['domain_uuid'] = $this->domain_uuid;
    +			$array['email_queue'][0]['hostname'] = gethostname();
    +			$array['email_queue'][0]['email_date'] = 'now()';
    +			$array['email_queue'][0]['email_from'] = $email_from;
    +			$array['email_queue'][0]['email_to'] = $this->recipients;
    +			$array['email_queue'][0]['email_subject'] = $this->subject;
    +			$array['email_queue'][0]['email_body'] = $this->body;
    +			$array['email_queue'][0]['email_status'] = 'waiting';
    +			$array['email_queue'][0]['email_retry_count'] = null;
    +			//$array['email_queue'][0]['email_action_before'] = $email_action_before;
    +			//$array['email_queue'][0]['email_action_after'] = $email_action_after;
    +
    +			//add email attachments
    +			if (is_array($this->attachments) && sizeof($this->attachments) > 0) {
    +				$y = 0;
    +				foreach ($this->attachments as $attachment) {
    +					//set the name of the file, determine extension
    +					if ($attachment['path'] && $attachment['name']) {
    +						if (file_exists($attachment['path'] && $attachment['name'])) {
    +							$attachment['type'] = strtolower(pathinfo($attachment['name'], PATHINFO_EXTENSION));
    +						}
    +					} elseif ($attachment['value']) {
    +						//old method
    +						if (strlen($attachment['value']) < 255 && file_exists($attachment['value'])) {
    +							$attachment['name'] = $attachment['name'] != '' ? $attachment['name'] : basename($attachment['value']);
    +							$attachment['path'] = pathinfo($attachment['value'], PATHINFO_DIRNAME);
    +							$attachment['type'] = strtolower(pathinfo($attachment['value'], PATHINFO_EXTENSION));
    +						}
    +					}
    +
    +					//set the mime type
    +					switch ($attachment['type']) {
    +						case "jpg":
    +						case "jpeg":
    +							$attachment['mime_type'] = 'image/jpeg';
    +							break;
    +						case "gif":
    +							$attachment['mime_type'] = 'image/gif';
    +							break;
    +						case "png":
    +							$attachment['mime_type'] = 'image/png';
    +							break;
    +						case "pdf":
    +							$attachment['mime_type'] = 'application/pdf';
    +							break;
    +						case "tif":
    +						case "tiff":
    +							$attachment['mime_type'] = 'image/tiff';
    +							break;
    +						case "mp3":
    +							$attachment['mime_type'] = 'audio/mpeg';
    +							break;
    +						case "wav":
    +							$attachment['mime_type'] = 'audio/x-wav';
    +							break;
    +						case "opus":
    +							$attachment['mime_type'] = 'audio/opus';
    +							break;
    +						case "ogg":
    +							$attachment['mime_type'] = 'audio/ogg';
    +							break;
    +						default:
    +							$attachment['mime_type'] = 'binary/octet-stream';
    +					}
    +
    +					//add the attachments to the array
    +					$array['email_queue_attachments'][$y]['email_queue_attachment_uuid'] = uuid();
    +					$array['email_queue_attachments'][$y]['email_queue_uuid'] = $email_queue_uuid;
    +					$array['email_queue_attachments'][$y]['domain_uuid'] = $this->domain_uuid;
    +					$array['email_queue_attachments'][$y]['email_attachment_mime_type'] = $attachment['mime_type'];
    +					$array['email_queue_attachments'][$y]['email_attachment_type'] = $attachment['type'];
    +					$array['email_queue_attachments'][$y]['email_attachment_name'] = $attachment['name'];
    +					$array['email_queue_attachments'][$y]['email_attachment_path'] = $attachment['path'];
    +					$array['email_queue_attachments'][$y]['email_attachment_base64'] = $attachment['base64'];
    +					$y++;
     				}
     			}
     
    -			//add the email to the queue
    -			if ($this->method == 'queue') {
    +			//add temporary permissions
    +			$p = permissions::new();
    +			$p->add("email_queue_add", 'temp');
    +			$p->add("email_queue_attachment_add", 'temp');
     
    -				//add the email_queue_uuid
    -				$email_queue_uuid = uuid();
    +			//save the dialplan
    +			$this->database->app_name = 'email';
    +			$this->database->app_uuid = 'e24b5dab-3bcc-42e8-99c1-19b0c558c2d7';
    +			$this->database->save($array);
    +			//$dialplan_response = $this->database->message;
    +			unset($array);
     
    -				//set the email from address and name
    -				$email_from = $this->from_address;
    -				if (!empty($this->from_name)) {
    -					$email_from = $this->from_name.'<'.$email_from.'>';
    +			//remove temporary permissions
    +			$p->delete("dialplan_add", 'temp');
    +			$p->delete("dialplan_detail_add", 'temp');
    +
    +			//return a human readable response for debugging
    +			if ($this->database->message['message'] == 'OK') {
    +				return "Added to queue";
    +			} else {
    +				//return the SQL server message
    +				return $this->database->message['message'];
    +			}
    +		}
    +
    +		//send the email directly
    +		if ($this->method == 'direct') {
    +			/*
    +			RECIPIENTS NOTE:
    +
    +				Pass in a single email address...
    +
    +					user@domain.com
    +
    +				Pass in a comma or semi-colon delimited string of e-mail addresses...
    +
    +					user@domain.com,user2@domain2.com,user3@domain3.com
    +					user@domain.com;user2@domain2.com;user3@domain3.com
    +
    +				Pass in a simple array of email addresses...
    +
    +					Array (
    +						[0] => user@domain.com
    +						[1] => user2@domain2.com
    +						[2] => user3@domain3.com
    +					)
    +
    +				Pass in a multi-dimentional array of addresses (delivery, address, name)...
    +
    +					Array (
    +						[0] => Array (
    +							[delivery] => to
    +							[address] => user@domain.com
    +							[name] => user 1
    +							)
    +						[1] => Array (
    +							[delivery] => cc
    +							[address] => user2@domain2.com
    +							[name] => user 2
    +							)
    +						[2] => Array (
    +							[delivery] => bcc
    +							[address] => user3@domain3.com
    +							[name] => user 3
    +							)
    +					)
    +
    +			ATTACHMENTS NOTE:
    +
    +				Pass in as many files as necessary in an array in the following format...
    +
    +					Array (
    +						[0] => Array (
    +							[mime_type] => image/jpeg (will be determined by file extension, if empty)
    +							[name] => filename.ext
    +							[path] => /source/folder/ (not used if base64 content)
    +							[base64] => file content as base64 (not used if name and path set)
    +							[cid] => content id of file attachment (only used if referencing attached files in body content)
    +							)
    +						[1] => Array (
    +							...
    +							)
    +					)
    +
    +			ERROR RESPONSE:
    +
    +				Error messages are stored in the variable passed into $this->error BY REFERENCE
    +			*/
    +
    +			try {
    +				//include the phpmailer classes
    +				include_once("resources/phpmailer/class.phpmailer.php");
    +				include_once("resources/phpmailer/class.smtp.php");
    +
    +				//use the email default settings
    +				if (!empty($this->settings->get('email', 'smtp_hostname'))) {
    +					$smtp['hostname'] = $this->settings->get('email', 'smtp_hostname');
     				}
    +				$smtp['host'] = $this->settings->get('email', 'smtp_host', '127.0.0.1');
    +				$smtp['port'] = (int)$this->settings->get('email', 'smtp_port', 0);
    +				$smtp['secure'] = $this->settings->get('email', 'smtp_secure');
    +				$smtp['auth'] = $this->settings->get('email', 'smtp_auth');
    +				$smtp['username'] = $this->settings->get('email', 'smtp_username');
    +				$smtp['password'] = $this->settings->get('email', 'smtp_password');
    +				$smtp['from'] = $this->settings->get('voicemail', 'smtp_from') ?? $this->settings->get('email', 'smtp_from');
    +				$smtp['from_name'] = $this->settings->get('voicemail', 'smtp_from_name') ?? $this->settings->get('email', 'smtp_from_name');
    +				$smtp['validate_certificate'] = $this->settings->get('email', 'smtp_validate_certificate', true);
    +				$smtp['crypto_method'] = $this->settings->get('email', 'smtp_crypto_method') ?? null;
     
    -				//prepare the array
    -				$array['email_queue'][0]['email_queue_uuid'] = $email_queue_uuid;
    -				$array['email_queue'][0]['domain_uuid'] = $this->domain_uuid;
    -				$array['email_queue'][0]['hostname'] = gethostname();
    -				$array['email_queue'][0]['email_date'] = 'now()';
    -				$array['email_queue'][0]['email_from'] = $email_from;
    -				$array['email_queue'][0]['email_to'] = $this->recipients;
    -				$array['email_queue'][0]['email_subject'] = $this->subject;
    -				$array['email_queue'][0]['email_body'] = $this->body;
    -				$array['email_queue'][0]['email_status'] = 'waiting';
    -				$array['email_queue'][0]['email_retry_count'] = null;
    -				//$array['email_queue'][0]['email_action_before'] = $email_action_before;
    -				//$array['email_queue'][0]['email_action_after'] = $email_action_after;
    -
    -				//add email attachments
    -				if (is_array($this->attachments) && sizeof($this->attachments) > 0) {
    -					$y = 0;
    -					foreach ($this->attachments as $attachment) {
    -						//set the name of the file, determine extension
    -						if ($attachment['path'] && $attachment['name']) {
    -							if (file_exists($attachment['path'] && $attachment['name'])) {
    -								$attachment['type'] = strtolower(pathinfo($attachment['name'], PATHINFO_EXTENSION));
    -							}
    +				//override the domain-specific smtp server settings, if any
    +				$sql = "select domain_setting_subcategory, domain_setting_value ";
    +				$sql .= "from v_domain_settings ";
    +				$sql .= "where domain_uuid = :domain_uuid ";
    +				$sql .= "and (domain_setting_category = 'email' or domain_setting_category = 'voicemail') ";
    +				$sql .= "and domain_setting_enabled = 'true' ";
    +				$parameters['domain_uuid'] = $this->domain_uuid;
    +				$result = $this->database->select($sql, $parameters, 'all');
    +				if (is_array($result) && @sizeof($result) != 0) {
    +					foreach ($result as $row) {
    +						if ($row['domain_setting_value'] != '') {
    +							$smtp[str_replace('smtp_', '', $row["domain_setting_subcategory"])] = $row['domain_setting_value'];
     						}
    -						else if ($attachment['value']) {
    -							//old method
    -							if (strlen($attachment['value']) < 255 && file_exists($attachment['value'])) {
    -								$attachment['name'] = $attachment['name'] != '' ? $attachment['name'] : basename($attachment['value']);
    -								$attachment['path'] = pathinfo($attachment['value'], PATHINFO_DIRNAME);
    -								$attachment['type'] = strtolower(pathinfo($attachment['value'], PATHINFO_EXTENSION));
    -							}
    -						}
    -
    -						//set the mime type
    -						switch ($attachment['type']) {
    -							case "jpg":
    -							case "jpeg":
    -								$attachment['mime_type'] = 'image/jpeg';
    -								break;
    -							case "gif":
    -								$attachment['mime_type'] = 'image/gif';
    -								break;
    -							case "png":
    -								$attachment['mime_type'] = 'image/png';
    -								break;
    -							case "pdf":
    -								$attachment['mime_type'] = 'application/pdf';
    -								break;
    -							case "tif":
    -							case "tiff":
    -								$attachment['mime_type'] = 'image/tiff';
    -								break;
    -							case "mp3":
    -								$attachment['mime_type'] = 'audio/mpeg';
    -								break;
    -							case "wav":
    -								$attachment['mime_type'] = 'audio/x-wav';
    -								break;
    -							case "opus":
    -								$attachment['mime_type'] = 'audio/opus';
    -								break;
    -							case "ogg":
    -								$attachment['mime_type'] = 'audio/ogg';
    -								break;
    -							default:
    -								$attachment['mime_type'] = 'binary/octet-stream';
    -						}
    -
    -						//add the attachments to the array
    -						$array['email_queue_attachments'][$y]['email_queue_attachment_uuid'] = uuid();
    -						$array['email_queue_attachments'][$y]['email_queue_uuid'] = $email_queue_uuid;
    -						$array['email_queue_attachments'][$y]['domain_uuid'] = $this->domain_uuid;
    -						$array['email_queue_attachments'][$y]['email_attachment_mime_type'] = $attachment['mime_type'];
    -						$array['email_queue_attachments'][$y]['email_attachment_type'] = $attachment['type'];
    -						$array['email_queue_attachments'][$y]['email_attachment_name'] = $attachment['name'];
    -						$array['email_queue_attachments'][$y]['email_attachment_path'] = $attachment['path'];
    -						$array['email_queue_attachments'][$y]['email_attachment_base64'] = $attachment['base64'];
    -						$y++;
     					}
     				}
    +				unset($sql, $parameters, $result, $row);
     
    -				//add temporary permissions
    -				$p = permissions::new();
    -				$p->add("email_queue_add", 'temp');
    -				$p->add("email_queue_attachment_add", 'temp');
    +				//value adjustments
    +				$smtp['auth'] = ($smtp['auth'] == "true") ? true : false;
    +				$smtp['password'] = ($smtp['password'] != '') ? $smtp['password'] : null;
    +				$smtp['secure'] = ($smtp['secure'] != "none") ? $smtp['secure'] : null;
    +				$smtp['username'] = ($smtp['username'] != '') ? $smtp['username'] : null;
     
    -				//save the dialplan
    -				$this->database->app_name = 'email';
    -				$this->database->app_uuid = 'e24b5dab-3bcc-42e8-99c1-19b0c558c2d7';
    -				$this->database->save($array);
    -				//$dialplan_response = $this->database->message;
    -				unset($array);
    +				//create the email object and set general settings
    +				$mail = new PHPMailer();
    +				$mail->IsSMTP();
    +				if (!empty($smtp['hostname'])) {
    +					$mail->Hostname = $smtp['hostname'];
    +				}
    +				$mail->Host = $smtp['host'];
    +				if (is_numeric($smtp['port'])) {
    +					$mail->Port = $smtp['port'];
    +				}
     
    -				//remove temporary permissions
    -				$p->delete("dialplan_add", 'temp');
    -				$p->delete("dialplan_detail_add", 'temp');
    -
    -				//return a human readable response for debugging
    -				if ($this->database->message['message'] == 'OK') {
    -					return "Added to queue";
    +				if ($smtp['auth'] == "true") {
    +					$mail->SMTPAuth = true;
    +					$mail->Username = $smtp['username'];
    +					$mail->Password = $smtp['password'];
     				} else {
    -					//return the SQL server message
    -					return $this->database->message['message'];
    +					$mail->SMTPAuth = false;
     				}
    -			}
     
    -			//send the email directly
    -			if ($this->method == 'direct') {
    -				/*
    -				RECIPIENTS NOTE:
    +				$smtp_secure = true;
    +				if ($smtp['secure'] == "") {
    +					$mail->SMTPSecure = 'none';
    +					$mail->SMTPAutoTLS = false;
    +					$smtp_secure = false;
    +				} elseif ($smtp['secure'] == "none") {
    +					$mail->SMTPSecure = 'none';
    +					$mail->SMTPAutoTLS = false;
    +					$smtp_secure = false;
    +				} else {
    +					$mail->SMTPSecure = $smtp['secure'];
    +				}
     
    -					Pass in a single email address...
    +				if ($smtp_secure && isset($smtp['validate_certificate']) && !$smtp['validate_certificate']) {
    +					//bypass certificate check e.g. for self-signed certificates
    +					$smtp_options['ssl']['verify_peer'] = false;
    +					$smtp_options['ssl']['verify_peer_name'] = false;
    +					$smtp_options['ssl']['allow_self_signed'] = true;
    +				}
     
    -						user@domain.com
    +				//used to set the SSL version
    +				if ($smtp_secure && isset($smtp['crypto_method'])) {
    +					$smtp_options['ssl']['crypto_method'] = $smtp['crypto_method'];
    +				}
     
    -					Pass in a comma or semi-colon delimited string of e-mail addresses...
    +				//add SMTP Options if the array exists
    +				if (is_array($smtp_options)) {
    +					$mail->SMTPOptions = $smtp_options;
    +				}
     
    -						user@domain.com,user2@domain2.com,user3@domain3.com
    -						user@domain.com;user2@domain2.com;user3@domain3.com
    +				$this->from_address = ($this->from_address != '') ? $this->from_address : $smtp['from'];
    +				$this->from_name = ($this->from_name != '') ? $this->from_name : $smtp['from_name'];
    +				$mail->SetFrom($this->from_address, $this->from_name);
    +				$mail->AddReplyTo($this->from_address, $this->from_name);
    +				$mail->Subject = $this->subject;
    +				$mail->MsgHTML($this->body);
    +				$mail->Priority = $this->priority;
    +				if ($this->read_confirmation) {
    +					$mail->AddCustomHeader('X-Confirm-Reading-To: ' . $this->from_address);
    +					$mail->AddCustomHeader('Return-Receipt-To: ' . $this->from_address);
    +					$mail->AddCustomHeader('Disposition-Notification-To: ' . $this->from_address);
    +				}
    +				if (is_numeric($this->debug_level) && $this->debug_level > 0) {
    +					$mail->SMTPDebug = $this->debug_level;
    +				}
    +				$mail->Timeout = 20; //set the timeout (seconds)
    +				$mail->SMTPKeepAlive = true; //don't close the connection between messages
     
    -					Pass in a simple array of email addresses...
    +				//add the email recipients
    +				$address_found = false;
    +				if (!is_array($this->recipients)) { // must be a single or delimited recipient address(s)
    +					$this->recipients = str_replace(' ', '', $this->recipients);
    +					$this->recipients = str_replace(',', ';', $this->recipients);
    +					$this->recipients = explode(';', $this->recipients); // convert to array of addresses
    +				}
     
    -						Array (
    -							[0] => user@domain.com
    -							[1] => user2@domain2.com
    -							[2] => user3@domain3.com
    -						)
    -
    -					Pass in a multi-dimentional array of addresses (delivery, address, name)...
    -
    -						Array (
    -							[0] => Array (
    -								[delivery] => to
    -								[address] => user@domain.com
    -								[name] => user 1
    -								)
    -							[1] => Array (
    -								[delivery] => cc
    -								[address] => user2@domain2.com
    -								[name] => user 2
    -								)
    -							[2] => Array (
    -								[delivery] => bcc
    -								[address] => user3@domain3.com
    -								[name] => user 3
    -								)
    -						)
    -
    -				ATTACHMENTS NOTE:
    -
    -					Pass in as many files as necessary in an array in the following format...
    -
    -						Array (
    -							[0] => Array (
    -								[mime_type] => image/jpeg (will be determined by file extension, if empty)
    -								[name] => filename.ext
    -								[path] => /source/folder/ (not used if base64 content)
    -								[base64] => file content as base64 (not used if name and path set)
    -								[cid] => content id of file attachment (only used if referencing attached files in body content)
    -								)
    -							[1] => Array (
    -								...
    -								)
    -						)
    -
    -				ERROR RESPONSE:
    -
    -					Error messages are stored in the variable passed into $this->error BY REFERENCE
    -				*/
    -
    -				try {
    -					//include the phpmailer classes
    -					include_once("resources/phpmailer/class.phpmailer.php");
    -					include_once("resources/phpmailer/class.smtp.php");
    -
    -					//use the email default settings
    -					if (!empty($this->settings->get('email','smtp_hostname'))) {
    -						$smtp['hostname'] = $this->settings->get('email','smtp_hostname');
    -					}
    -					$smtp['host'] = $this->settings->get('email','smtp_host', '127.0.0.1');
    -					$smtp['port'] = (int)$this->settings->get('email','smtp_port', 0);
    -					$smtp['secure'] 	= $this->settings->get('email','smtp_secure');
    -					$smtp['auth'] 		= $this->settings->get('email','smtp_auth');
    -					$smtp['username'] 	= $this->settings->get('email','smtp_username');
    -					$smtp['password'] 	= $this->settings->get('email','smtp_password');
    -					$smtp['from'] 		= $this->settings->get('voicemail','smtp_from') ?? $this->settings->get('email','smtp_from');
    -					$smtp['from_name'] 	= $this->settings->get('voicemail','smtp_from_name') ?? $this->settings->get('email','smtp_from_name');
    -					$smtp['validate_certificate'] = $this->settings->get('email','smtp_validate_certificate', true);
    -					$smtp['crypto_method'] = $this->settings->get('email','smtp_crypto_method') ?? null;
    -
    -					//override the domain-specific smtp server settings, if any
    -					$sql = "select domain_setting_subcategory, domain_setting_value ";
    -					$sql .= "from v_domain_settings ";
    -					$sql .= "where domain_uuid = :domain_uuid ";
    -					$sql .= "and (domain_setting_category = 'email' or domain_setting_category = 'voicemail') ";
    -					$sql .= "and domain_setting_enabled = 'true' ";
    -					$parameters['domain_uuid'] = $this->domain_uuid;
    -					$result = $this->database->select($sql, $parameters, 'all');
    -					if (is_array($result) && @sizeof($result) != 0) {
    -						foreach ($result as $row) {
    -							if ($row['domain_setting_value'] != '') {
    -								$smtp[str_replace('smtp_','',$row["domain_setting_subcategory"])] = $row['domain_setting_value'];
    +				foreach ($this->recipients as $recipient) {
    +					if (is_array($recipient)) { // check if each recipient has multiple fields
    +						if ($recipient["address"] != '' && valid_email($recipient["address"])) { // check if valid address
    +							switch ($recipient["delivery"]) {
    +								case "cc" :
    +									$mail->AddCC($recipient["address"], ($recipient["name"]) ? $recipient["name"] : $recipient["address"]);
    +									break;
    +								case "bcc" :
    +									$mail->AddBCC($recipient["address"], ($recipient["name"]) ? $recipient["name"] : $recipient["address"]);
    +									break;
    +								default :
    +									$mail->AddAddress($recipient["address"], ($recipient["name"]) ? $recipient["name"] : $recipient["address"]);
     							}
    -						}
    -					}
    -					unset($sql, $parameters, $result, $row);
    -
    -					//value adjustments
    -					$smtp['auth']		= ($smtp['auth'] == "true") ? true : false;
    -					$smtp['password']	= ($smtp['password'] != '') ? $smtp['password'] : null;
    -					$smtp['secure']		= ($smtp['secure'] != "none") ? $smtp['secure'] : null;
    -					$smtp['username']	= ($smtp['username'] != '') ? $smtp['username'] : null;
    -
    -					//create the email object and set general settings
    -					$mail = new PHPMailer();
    -					$mail->IsSMTP();
    -					if (!empty($smtp['hostname'])) {
    -						$mail->Hostname = $smtp['hostname'];
    -					}
    -					$mail->Host = $smtp['host'];
    -					if (is_numeric($smtp['port'])) {
    -						$mail->Port = $smtp['port'];
    -					}
    -
    -					if ($smtp['auth'] == "true") {
    -						$mail->SMTPAuth = true;
    -						$mail->Username = $smtp['username'];
    -						$mail->Password = $smtp['password'];
    -					}
    -					else {
    -						$mail->SMTPAuth = false;
    -					}
    -
    -					$smtp_secure = true;
    -					if ($smtp['secure'] == "") {
    -						$mail->SMTPSecure = 'none';
    -						$mail->SMTPAutoTLS = false;
    -						$smtp_secure = false;
    -					}
    -					elseif ($smtp['secure']  == "none") {
    -						$mail->SMTPSecure = 'none';
    -						$mail->SMTPAutoTLS = false;
    -						$smtp_secure = false;
    -					}
    -					else {
    -						$mail->SMTPSecure = $smtp['secure'];
    -					}
    -
    -					if ($smtp_secure && isset($smtp['validate_certificate']) && !$smtp['validate_certificate']) {
    -						//bypass certificate check e.g. for self-signed certificates
    -						$smtp_options['ssl']['verify_peer'] = false;
    -						$smtp_options['ssl']['verify_peer_name'] = false;
    -						$smtp_options['ssl']['allow_self_signed'] = true;
    -					}
    -
    -					//used to set the SSL version
    -					if ($smtp_secure && isset($smtp['crypto_method'])) {
    -						$smtp_options['ssl']['crypto_method'] = $smtp['crypto_method'];
    -					}
    -
    -					//add SMTP Options if the array exists
    -					if (is_array($smtp_options)) {
    -						$mail->SMTPOptions = $smtp_options;
    -					}
    -
    -					$this->from_address = ($this->from_address != '') ? $this->from_address : $smtp['from'];
    -					$this->from_name = ($this->from_name != '') ? $this->from_name : $smtp['from_name'];
    -					$mail->SetFrom($this->from_address, $this->from_name);
    -					$mail->AddReplyTo($this->from_address, $this->from_name);
    -					$mail->Subject = $this->subject;
    -					$mail->MsgHTML($this->body);
    -					$mail->Priority = $this->priority;
    -					if ($this->read_confirmation) {
    -						$mail->AddCustomHeader('X-Confirm-Reading-To: '.$this->from_address);
    -						$mail->AddCustomHeader('Return-Receipt-To: '.$this->from_address);
    -						$mail->AddCustomHeader('Disposition-Notification-To: '.$this->from_address);
    -					}
    -					if (is_numeric($this->debug_level) && $this->debug_level > 0) {
    -						$mail->SMTPDebug = $this->debug_level;
    -					}
    -					$mail->Timeout       =   20; //set the timeout (seconds)
    -    					$mail->SMTPKeepAlive = true; //don't close the connection between messages
    -
    -					//add the email recipients
    -					$address_found = false;
    -					if (!is_array($this->recipients)) { // must be a single or delimited recipient address(s)
    -						$this->recipients = str_replace(' ', '', $this->recipients);
    -						$this->recipients = str_replace(',', ';', $this->recipients);
    -						$this->recipients = explode(';', $this->recipients); // convert to array of addresses
    -					}
    -
    -					foreach ($this->recipients as $recipient) {
    -						if (is_array($recipient)) { // check if each recipient has multiple fields
    -							if ($recipient["address"] != '' && valid_email($recipient["address"])) { // check if valid address
    -								switch ($recipient["delivery"]) {
    -									case "cc" :		$mail->AddCC($recipient["address"], ($recipient["name"]) ? $recipient["name"] : $recipient["address"]);			break;
    -									case "bcc" :	$mail->AddBCC($recipient["address"], ($recipient["name"]) ? $recipient["name"] : $recipient["address"]);			break;
    -									default :		$mail->AddAddress($recipient["address"], ($recipient["name"]) ? $recipient["name"] : $recipient["address"]);
    -								}
    -								$address_found = true;
    -							}
    -						}
    -						else if ($recipient != '' && valid_email($recipient)) { // check if recipient value is simply (only) an address
    -							$mail->AddAddress($recipient);
     							$address_found = true;
     						}
    +					} elseif ($recipient != '' && valid_email($recipient)) { // check if recipient value is simply (only) an address
    +						$mail->AddAddress($recipient);
    +						$address_found = true;
     					}
    -
    -					if (!$address_found) {
    -						$this->error = "No valid e-mail address provided.";
    -						return false;
    -					}
    -
    -					//add email attachments
    -					if (is_array($this->attachments) && sizeof($this->attachments) > 0) {
    -						foreach ($this->attachments as $attachment) {
    -
    -							//add the attachments
    -							if (file_exists($attachment['path'].'/'.$attachment['name'])) {
    -								$mail->AddAttachment($attachment['path'].'/'.$attachment['name'], $attachment['name'], 'base64', $attachment['mime_type']);
    -							}
    -							else {
    -								if ($attachment['base64']) {
    -									if ($attachment['cid']) {
    -										$mail->addStringEmbeddedImage(base64_decode($attachment['base64']), $attachment['cid'], $attachment['name'], 'base64', $attachment['mime_type']);
    -									}
    -									else {
    -										$mail->AddStringAttachment(base64_decode($attachment['base64']), $attachment['name'], 'base64', $attachment['mime_type']);
    -									}
    -								}
    -							}
    -						}
    -					}
    -
    -					//save output to a buffer
    -					ob_start();
    -
    -					//send the email
    -					$mail_status = $mail->Send();
    -
    -					//get the output buffer
    -					$this->response = ob_get_clean();
    -
    -					//send the email
    -					if (!$mail_status) {
    -						if (isset($mail->ErrorInfo) && !empty($mail->ErrorInfo)) {
    -							$this->error = $mail->ErrorInfo;
    -						}
    -						return false;
    -					}
    -
    -					//cleanup the mail object
    -					$mail->ClearAddresses();
    -					$mail->SmtpClose();
    -					unset($mail);
    -					return true;
    -
     				}
    -				catch (Exception $e) {
    -					$this->error = $mail->ErrorInfo;
    +
    +				if (!$address_found) {
    +					$this->error = "No valid e-mail address provided.";
     					return false;
     				}
     
    -			}
    -		}
    +				//add email attachments
    +				if (is_array($this->attachments) && sizeof($this->attachments) > 0) {
    +					foreach ($this->attachments as $attachment) {
     
    +						//add the attachments
    +						if (file_exists($attachment['path'] . '/' . $attachment['name'])) {
    +							$mail->AddAttachment($attachment['path'] . '/' . $attachment['name'], $attachment['name'], 'base64', $attachment['mime_type']);
    +						} else {
    +							if ($attachment['base64']) {
    +								if ($attachment['cid']) {
    +									$mail->addStringEmbeddedImage(base64_decode($attachment['base64']), $attachment['cid'], $attachment['name'], 'base64', $attachment['mime_type']);
    +								} else {
    +									$mail->AddStringAttachment(base64_decode($attachment['base64']), $attachment['name'], 'base64', $attachment['mime_type']);
    +								}
    +							}
    +						}
    +					}
    +				}
    +
    +				//save output to a buffer
    +				ob_start();
    +
    +				//send the email
    +				$mail_status = $mail->Send();
    +
    +				//get the output buffer
    +				$this->response = ob_get_clean();
    +
    +				//send the email
    +				if (!$mail_status) {
    +					if (isset($mail->ErrorInfo) && !empty($mail->ErrorInfo)) {
    +						$this->error = $mail->ErrorInfo;
    +					}
    +					return false;
    +				}
    +
    +				//cleanup the mail object
    +				$mail->ClearAddresses();
    +				$mail->SmtpClose();
    +				unset($mail);
    +				return true;
    +
    +			} catch (Exception $e) {
    +				$this->error = $mail->ErrorInfo;
    +				return false;
    +			}
    +
    +		}
     	}
     
    +}
    +
     
     /*
     $email = new email;
    diff --git a/resources/classes/event_socket.php b/resources/classes/event_socket.php
    index 8a29c4422c..077968b415 100644
    --- a/resources/classes/event_socket.php
    +++ b/resources/classes/event_socket.php
    @@ -37,6 +37,7 @@ class buffer {
     		return $tmp;
     	}
     }
    +
     //$b = new buffer;
     //$b->append("hello\nworld\n");
     //print($b->read_line());
    @@ -44,23 +45,24 @@ class buffer {
     
     /**
      * Subscribes to the event socket of the FreeSWITCH (c) Event Socket Server
    + *
      * @depends buffer::class
      */
     class event_socket {
    +	private static $socket = null;
    +	public $fp;
     	/**
     	 * Used as a flag to determine if the socket should be created automatically
    +	 *
     	 * @var bool
     	 */
     	protected $auto_create;
    -
     	private $buffer;
    -	public $fp;
    -
    -	private static $socket = null;
     	private $config;
     
     	/**
     	 * Create a new connection to the socket
    +	 *
     	 * @param resource|false $fp
     	 */
     	public function __construct($fp = false, ?config $config = null) {
    @@ -71,15 +73,59 @@ class event_socket {
     	}
     
     	/**
    -	 * Ensures a closed connection on destruction of object
    +	 * Sends an API command on the socket
    +	 *
    +	 * @param string $api_cmd
    +	 *
    +	 * @return string|false Response from server or false if failed
     	 */
    -	public function __destructor() {
    -		$this->close();
    +	public static function api(string $api_cmd) {
    +		return self::command('api ' . $api_cmd);
    +	}
    +
    +	/**
    +	 * Sends a command on the socket blocking for a response
    +	 *
    +	 * @param string $cmd
    +	 *
    +	 * @return string|false Response from server or false if failed
    +	 */
    +	public static function command(string $cmd) {
    +		return self::create()->request($cmd);
    +	}
    +
    +	/**
    +	 * Send a command to the FreeSWITCH Event Socket Server
    +	 * 

    Multi-line commands can be sent when separated by '\n'

    + * + * @param string $cmd Command to send through the socket + * + * @return mixed Returns the response from FreeSWITCH or false if not connected + * @depends read_event() + */ + public function request($cmd) { + if (!$this->connected()) { + return false; + } + + $cmd_array = explode("\n", $cmd); + foreach ($cmd_array as $value) { + fputs($this->fp, $value . "\n"); + } + fputs($this->fp, "\n"); //second line feed to end the headers + + $event = $this->read_event(); + + if (array_key_exists('$', $event)) { + return $event['$']; + } + return $event; } /** * Read the event body from the socket - * @return string|false Content body or false if not connected or empty message + * + * @return mixed Content body or false if not connected or empty message * @depends buffer::class */ public function read_event() { @@ -88,7 +134,7 @@ class event_socket { } $b = $this->buffer; - $content = array(); + $content = []; while (true) { $line = $b->read_line(); @@ -96,7 +142,7 @@ class event_socket { if ($line === '') { break; } - list($key, $value) = explode(':', $line, 2); + [$key, $value] = explode(':', $line, 2); $content[trim($key)] = trim($value); } @@ -132,14 +178,41 @@ class event_socket { return $content; } + /** + * Create uses a singleton design to return a connected socket to the FreeSWITCH Event Socket Layer + * + * @param string $host Host or IP address of FreeSWITCH event socket server. Defaults to 127.0.0.1 + * @param string $port Port number of FreeSWITCH event socket server. Defaults to 8021 + * @param string $password Password of FreeSWITCH event socket server. Defaults to ClueCon + * @param int $timeout_microseconds Number of microseconds before timeout is triggered on socket + * + * @return self + * @global array $conf Global configuration used in config.conf + */ + public static function create($host = null, $port = null, $password = null, $timeout_microseconds = 30000): self { + //create the event socket object + if (self::$socket === null) { + self::$socket = new event_socket(); + } + //attempt to connect it + if (!self::$socket->connected()) { + self::$socket->connect($host, $port, $password, $timeout_microseconds); + } + return self::$socket; + } + /** * Connect to the FreeSWITCH (c) event socket server *

    If the configuration is not loaded then the defaults of * host 127.0.0.1, port of 8021, and default password of ClueCon will be used

    - * @param null|string $host Host or IP address of FreeSWITCH event socket server. Defaults to 127.0.0.1 - * @param null|string|int $port Port number of FreeSWITCH event socket server. Defaults to 8021 - * @param null|string $password Password of FreeSWITCH event socket server. Defaults to ClueCon - * @param int $timeout_microseconds Number of microseconds before timeout is triggered on socket. Defaults to 30,000 + * + * @param null|string $host Host or IP address of FreeSWITCH event socket server. Defaults to + * 127.0.0.1 + * @param null|string|int $port Port number of FreeSWITCH event socket server. Defaults to 8021 + * @param null|string $password Password of FreeSWITCH event socket server. Defaults to ClueCon + * @param int $timeout_microseconds Number of microseconds before timeout is triggered on socket. + * Defaults to 30,000 + * * @return bool Returns true on success or false if not connected */ public function connect($host = null, $port = null, $password = null, $timeout_microseconds = 30000) { @@ -165,7 +238,7 @@ class event_socket { //wait auth request and send a response while ($this->connected()) { $event = $this->read_event(); - if(($event['Content-Type'] ?? '') === 'auth/request'){ + if (($event['Content-Type'] ?? '') === 'auth/request') { fputs($this->fp, "auth $password\n\n"); break; } @@ -186,8 +259,60 @@ class event_socket { return $this->connected(); } + /** + * Sends an API command to FreeSWITCH using asynchronous (non-blocking) mode + * + * @param string $cmd API command to send + * + * @returns string $job_id the Job ID for tracking completion status + */ + public static function async(string $cmd) { + return self::command('bgapi ' . $cmd); + } + + /** + * Ensures a closed connection on destruction of object + */ + public function __destructor() { + $this->close(); + } + + /** + * Close the socket connection with the FreeSWITCH Event Socket Server. + * + * @return void + */ + public function close() { + //fp is public access so ensure it is a resource before closing it + if (is_resource($this->fp)) { + try { + fclose($this->fp); + } catch (Exception $t) { + //report it + trigger_error("event_socket failed to close socket", E_USER_WARNING); + } + } else { + //log an error if fp was set to something other than a resource + if ($this->fp !== false) { + trigger_error("event_socket not a resource", E_USER_ERROR); + } + } + //force fp to be false + $this->fp = false; + } + + /** + * alias of connected + * + * @return bool + */ + public function is_connected(): bool { + return $this->connected(); + } + /** * Tests if connected to the FreeSWITCH Event Socket Server + * * @return bool Returns true when connected or false when not connected */ public function connected(): bool { @@ -203,124 +328,20 @@ class event_socket { return true; } - /** - * alias of connected - * @return bool - */ - public function is_connected(): bool { - return $this->connected(); - } - - /** - * Send a command to the FreeSWITCH Event Socket Server - *

    Multi-line commands can be sent when separated by '\n'

    - * @param string $cmd Command to send through the socket - * @return mixed Returns the response from FreeSWITCH or false if not connected - * @depends read_event() - */ - public function request($cmd) { - if (!$this->connected()) { - return false; - } - - $cmd_array = explode("\n", $cmd); - foreach ($cmd_array as $value) { - fputs($this->fp, $value."\n"); - } - fputs($this->fp, "\n"); //second line feed to end the headers - - $event = $this->read_event(); - - if (array_key_exists('$', $event)) { - return $event['$']; - } - return $event; - } - /** * Sets the current socket resource returning the old + * * @param resource|bool $fp Sets the current FreeSWITCH resource + * * @return mixed Returns the original resource * @deprecated since version 5.1 */ - public function reset_fp($fp = false){ + public function reset_fp($fp = false) { $tmp = $this->fp; $this->fp = $fp; return $tmp; } - - /** - * Closes the socket - */ - public function close() { - //fp is public access so ensure it is a resource before closing it - if (is_resource($this->fp)) { - try { - fclose($this->fp); - } catch (\Exception $t) { - //report it - trigger_error("event_socket failed to close socket", E_USER_WARNING); - } - } else { - //log an error if fp was set to something other than a resource - if ($this->fp !== false) { - trigger_error("event_socket not a resource", E_USER_ERROR); - } - } - //force fp to be false - $this->fp = false; - } - - /** - * Create uses a singleton design to return a connected socket to the FreeSWITCH Event Socket Layer - * @global array $conf Global configuration used in config.conf - * @param string $host Host or IP address of FreeSWITCH event socket server. Defaults to 127.0.0.1 - * @param string $port Port number of FreeSWITCH event socket server. Defaults to 8021 - * @param string $password Password of FreeSWITCH event socket server. Defaults to ClueCon - * @param int $timeout_microseconds Number of microseconds before timeout is triggered on socket - * @return self - */ - public static function create($host = null, $port = null, $password = null, $timeout_microseconds = 30000): self { - //create the event socket object - if (self::$socket === null) { - self::$socket = new event_socket(); - } - //attempt to connect it - if(!self::$socket->connected()) { - self::$socket->connect($host, $port, $password, $timeout_microseconds); - } - return self::$socket; - } - - /** - * Sends a command on the socket blocking for a response - * @param string $cmd - * @return string|false Response from server or false if failed - */ - public static function command(string $cmd) { - return self::create()->request($cmd); - } - - /** - * Sends an API command on the socket - * @param string $api_cmd - * @return string|false Response from server or false if failed - */ - public static function api(string $api_cmd) { - return self::command('api '.$api_cmd); - } - - /** - * Sends an API command to FreeSWITCH using asynchronous (non-blocking) mode - * @param string $cmd API command to send - * @returns string $job_id the Job ID for tracking completion status - */ - public static function async(string $cmd) { - return self::command('bgapi '.$cmd); - } } // $esl = event_socket::create('127.0.0.1', 8021, 'ClueCon'); // print($esl->request('api sofia status')); - -?> diff --git a/resources/classes/file.php b/resources/classes/file.php index 02344545be..45f9e01777 100644 --- a/resources/classes/file.php +++ b/resources/classes/file.php @@ -1,58 +1,122 @@ domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? ''; - $this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? ''; + public function __construct(array $setting_array = []) { + //set domain and user UUIDs + $this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? ''; + $this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? ''; - //set objects - $this->database = $setting_array['database'] ?? database::new(); - $this->settings = $setting_array['settings'] ?? new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]); + //set objects + $this->database = $setting_array['database'] ?? database::new(); + $this->settings = $setting_array['settings'] ?? new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]); } /** - * Glob search for a list of files - * @var string $dir this is the directory to scan - * @var boolean $recursive get the sub directories - * @return array list of files or an empty array if not found + * Returns an array of sound files. + * + * This method retrieves a list of sound files based on the provided language, + * dialect, and voice settings. If no specific values are provided, default values + * will be used. + * + * @param string $language The desired language (default: 'en'). + * @param string $dialect The desired dialect (default: 'us'). + * @param string $voice The desired voice (default: 'callie'). + * + * @return array An array of sound files. + */ + public function sounds($language = 'en', $dialect = 'us', $voice = 'callie') { + //define an empty array + $array = []; + + //set default values + if (!isset($language)) { + $language = 'en'; + } + if (!isset($dialect)) { + $dialect = 'us'; + } + if (!isset($voice)) { + $voice = 'callie'; + } + + //set the variables + if (!empty($this->settings->get('switch', 'sounds')) && file_exists($this->settings->get('switch', 'sounds'))) { + $dir = $this->settings->get('switch', 'sounds') . '/' . $language . '/' . $dialect . '/' . $voice; + $rate = '8000'; + $files = $this->glob($dir . '/*/' . $rate, true); + } + + //loop through the languages + if (!empty($files)) { + foreach ($files as $file) { + $file = substr($file, strlen($dir) + 1); + $file = str_replace("/" . $rate, "", $file); + $array[] = $file; + } + } + + //return the list of sounds + return $array; + } + + /** + * Finds files in the specified directory. + * + * This method recursively searches for files and directories within the given + * directory, returning an array of paths if found. If no recursion is performed, + * only the files directly within the directory are returned. + * + * @param string $dir The directory path to search. + * @param bool $recursive Whether to recursively traverse subdirectories. + * + * @return array An array of file paths found in the specified directory. */ public function glob($dir, $recursive): array { $files = []; @@ -60,7 +124,7 @@ class file { $tree = glob(rtrim($dir, '/') . '/*'); if ($recursive) { if (is_array($tree)) { - foreach($tree as $file) { + foreach ($tree as $file) { if (is_dir($file)) { if ($recursive == true) { $files[] = $this->glob($file, $recursive); @@ -71,41 +135,8 @@ class file { } } } - return $files; } - } - - - /** - * Get the sounds list of search as a relative path without the rate - */ - public function sounds($language = 'en', $dialect = 'us', $voice = 'callie') { - //define an empty array - $array = []; - - //set default values - if (!isset($language)) { $language = 'en'; } - if (!isset($dialect)) { $dialect = 'us'; } - if (!isset($voice)) { $voice = 'callie'; } - - //set the variables - if (!empty($this->settings->get('switch', 'sounds')) && file_exists($this->settings->get('switch', 'sounds'))) { - $dir = $this->settings->get('switch', 'sounds').'/'.$language.'/'.$dialect.'/'.$voice; - $rate = '8000'; - $files = $this->glob($dir.'/*/'.$rate, true); - } - - //loop through the languages - if (!empty($files)) { - foreach($files as $file) { - $file = substr($file, strlen($dir)+1); - $file = str_replace("/".$rate, "", $file); - $array[] = $file; - } - } - - //return the list of sounds - return $array; + return $files; } } @@ -116,5 +147,3 @@ class file { $files = $file->sounds(); print_r($files); */ - -?> diff --git a/resources/classes/filter_chain.php b/resources/classes/filter_chain.php index 949e58cdb8..5c245aba2c 100644 --- a/resources/classes/filter_chain.php +++ b/resources/classes/filter_chain.php @@ -35,7 +35,9 @@ final class filter_chain { /** * Builds a filter chain link for filter objects + * * @param array $filters Array of filter objects + * * @return filter */ public static function or_link(array $filters): filter { @@ -84,6 +86,13 @@ final class filter_chain { return $chain; } + /** + * Builds a filter chain link for filter objects + * + * @param array $filters Array of filter objects + * + * @return filter + */ public static function and_link(array $filters): filter { return new class($filters) implements filter { private $filters; @@ -98,7 +107,7 @@ final class filter_chain { // Check if a filter requires a null to be returned if ($result === null) { return null; - } elseif(!$result) { + } elseif (!$result) { return false; } } diff --git a/resources/classes/google_authenticator.php b/resources/classes/google_authenticator.php index 99f30b07a8..e61d95bf8b 100644 --- a/resources/classes/google_authenticator.php +++ b/resources/classes/google_authenticator.php @@ -25,61 +25,98 @@ class google_authenticator { self::$PIN_MODULO = pow(10, self::$PASS_CODE_LENGTH); } - public function checkCode($secret,$code) { + /** + * Checks if the provided code is valid based on the secret and time. + * + * @param string $secret The secret to verify against. + * @param string $code The code to check for validity. + * + * @return bool True if the code is valid, false otherwise. + */ + public function checkCode($secret, $code) { $time = floor(time() / 30); - for ( $i = -1; $i <= 1; $i++) { - if ($this->getCode($secret,$time + $i) == $code) { + for ($i = -1; $i <= 1; $i++) { + if ($this->getCode($secret, $time + $i) == $code) { return true; } } return false; } - public function getCode($secret,$time = null) { + /** + * Generates a PIN code based on the provided secret and time. + * + * @param string $secret The secret to use for generating the code. + * @param int|null $time The current time in seconds since the Unix epoch. Defaults to the current time divided + * by 30. + * + * @return string A six-digit PIN code. + */ + public function getCode($secret, $time = null) { if (!$time) { $time = floor(time() / 30); } - $base32 = new base2n(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE); + $base32 = new base2n(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true); $secret = $base32->decode($secret); $time = pack("N", $time); - $time = str_pad($time,8, chr(0), STR_PAD_LEFT); + $time = str_pad($time, 8, chr(0), STR_PAD_LEFT); - $hash = hash_hmac('sha1',$time,$secret,true); - $offset = ord(substr($hash,-1)); + $hash = hash_hmac('sha1', $time, $secret, true); + $offset = ord(substr($hash, -1)); $offset = $offset & 0xF; $truncatedHash = self::hashToInt($hash, $offset) & 0x7FFFFFFF; - $pinValue = str_pad($truncatedHash % self::$PIN_MODULO,6,"0",STR_PAD_LEFT);; + $pinValue = str_pad($truncatedHash % self::$PIN_MODULO, 6, "0", STR_PAD_LEFT); return $pinValue; } + /** + * Converts a byte array to an integer value. + * + * @param string $bytes The byte array to convert. + * @param int $start The starting position in the byte array. + * + * @return int The converted integer value. + */ protected function hashToInt($bytes, $start) { $input = substr($bytes, $start, strlen($bytes) - $start); - $val2 = unpack("N",substr($input,0,4)); + $val2 = unpack("N", substr($input, 0, 4)); return $val2[1]; } + /** + * Generates a URL for the Google QR code. + * + * @param string $user The user's username. + * @param string $hostname The hostname of the service. + * @param string $secret The secret to encode in the QR code. + * + * @return string A URL that can be used to generate a QR code with the provided secret. + */ public function getUrl($user, $hostname, $secret) { $url = sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret); $encoder = "https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl="; - $encoderURL = sprintf( "%sotpauth://totp/%s@%s&secret=%s",$encoder, $user, $hostname, $secret); + $encoderURL = sprintf("%sotpauth://totp/%s@%s&secret=%s", $encoder, $user, $hostname, $secret); return $encoderURL; } + /** + * Generates a secret based on random characters and converts it to Base32. + * + * @return string The generated secret in Base32 format. + */ public function generateSecret() { $secret = ""; - for($i = 1; $i<= self::$SECRET_LENGTH;$i++) { - $c = rand(0,255); - $secret .= pack("c",$c); + for ($i = 1; $i <= self::$SECRET_LENGTH; $i++) { + $c = rand(0, 255); + $secret .= pack("c", $c); } - $base32 = new base2n(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE); - return $base32->encode($secret); + $base32 = new base2n(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true); + return $base32->encode($secret); } } - -?> diff --git a/resources/classes/groups.php b/resources/classes/groups.php index e1e0bec385..f11c076f3a 100644 --- a/resources/classes/groups.php +++ b/resources/classes/groups.php @@ -28,516 +28,557 @@ * groups class provides methods for add, delete groups, and add default groups * */ - class groups { +class groups { - /** - * declare constant variables - */ - const app_name = 'groups'; - const app_uuid = '2caf27b0-540a-43d5-bb9b-c9871a1e4f84'; + /** + * declare constant variables + */ + const app_name = 'groups'; + const app_uuid = '2caf27b0-540a-43d5-bb9b-c9871a1e4f84'; - /** - * declare public variables - */ - public $group_uuid; - public $group_level; + /** + * declare public variables + */ + public $group_uuid; + public $group_level; - /** - * declare private variables - */ - private $database; - private $groups; - private $name; - private $table; - private $toggle_field; - private $toggle_values; - private $location; - private $user_uuid; - private $domain_uuid; + /** + * declare private variables + */ + private $database; + private $groups; + private $name; + private $table; + private $toggle_field; + private $toggle_values; + private $location; + private $user_uuid; + private $domain_uuid; - /** - * called when the object is created - */ - public function __construct(?database $database = null, $domain_uuid = null, $user_uuid = null) { + /** + * Initializes the object with database connection, domain UUID and user UUID. + * + * @param database|null $database Database instance or null to use default + * @param string|null $domain_uuid Domain UUID or null to set later + * @param string|null $user_uuid User UUID or null to set later + */ + public function __construct(?database $database = null, $domain_uuid = null, $user_uuid = null) { - //handle the database object - $this->database = $database ?? database::new(); + //handle the database object + $this->database = $database ?? database::new(); - //set the domain_uuid - if (is_uuid($domain_uuid)) { - $this->domain_uuid = $domain_uuid; - } + //set the domain_uuid + if (is_uuid($domain_uuid)) { + $this->domain_uuid = $domain_uuid; + } - //set the user_uuid - if (is_uuid($user_uuid)) { - $this->user_uuid = $user_uuid; - } + //set the user_uuid + if (is_uuid($user_uuid)) { + $this->user_uuid = $user_uuid; + } - //get the list of groups the user is a member of - if (!empty($this->domain_uuid) && !empty($this->user_uuid)) { - //get the groups and save them to the groups variable - $this->groups = $this->assigned(); + //get the list of groups the user is a member of + if (!empty($this->domain_uuid) && !empty($this->user_uuid)) { + //get the groups and save them to the groups variable + $this->groups = $this->assigned(); - //get the users group level - $group_level = 0; - foreach ($this->groups as $row) { - if ($this->group_level < $row['group_level']) { - $this->group_level = $row['group_level']; - } + //get the users group level + $group_level = 0; + foreach ($this->groups as $row) { + if ($this->group_level < $row['group_level']) { + $this->group_level = $row['group_level']; } } } - - /** - * get the list of groups the user is assigned to - */ - public function get_groups() { - //return the groups - return $this->groups; - } - - /** - * delete rows from the database - */ - public function delete($records) { - //assign the variables - $this->name = 'group'; - $this->table = 'groups'; - $this->location = 'groups.php'; - - if (permission_exists($this->name.'_delete')) { - - //add multi-lingual support - $language = new text; - $text = $language->get(); - - //validate the token - $token = new token; - if (!$token->validate($_SERVER['PHP_SELF'])) { - message::add($text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //delete multiple records - if (is_array($records) && @sizeof($records) != 0) { - //build array of checked records - foreach ($records as $x => $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - $array[$this->table][$x][$this->name.'_uuid'] = $record['uuid']; - $array['group_permissions'][$x][$this->name.'_uuid'] = $record['uuid']; - } - } - - //delete the checked rows - if (is_array($array) && @sizeof($array) != 0) { - - //grant temporary permissions - $p = permissions::new(); - $p->add('group_permission_delete', 'temp'); - - //execute delete - $this->database->delete($array); - unset($array); - - //revoke temporary permissions - $p->delete('group_permission_delete', 'temp'); - - //set message - message::add($text['message-delete']); - } - unset($records); - } - } - } - - public function delete_members($records) { - //assign the variables - $this->name = 'group_member'; - $this->table = 'user_groups'; - $this->location = 'group_members.php?group_uuid='.$this->group_uuid; - - if (permission_exists($this->name.'_delete')) { - - //add multi-lingual support - $language = new text; - $text = $language->get(); - - //validate the token - $token = new token; - if (!$token->validate($_SERVER['PHP_SELF'])) { - message::add($text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //delete multiple records - if (is_array($records) && @sizeof($records) != 0) { - //build array of checked records - foreach ($records as $x => $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - $array[$this->table][$x]['user_uuid'] = $record['uuid']; - $array[$this->table][$x]['group_uuid'] = $this->group_uuid; - } - } - - //delete the checked rows - if (is_array($array) && @sizeof($array) != 0) { - - //grant temporary permissions - $p = permissions::new(); - $p->add('user_group_delete', 'temp'); - - //execute delete - $this->database->delete($array); - unset($array); - - //revoke temporary permissions - $p->delete('user_group_delete', 'temp'); - - //set message - message::add($text['message-delete']); - } - unset($records); - } - } - } - - /** - * toggle a field between two values - */ - public function toggle($records) { - //assign the variables - $this->name = 'group'; - $this->table = 'groups'; - $this->toggle_field = 'group_protected'; - $this->toggle_values = ['true','false']; - $this->location = 'groups.php'; - - if (permission_exists($this->name.'_edit')) { - - //add multi-lingual support - $language = new text; - $text = $language->get(); - - //validate the token - $token = new token; - if (!$token->validate($_SERVER['PHP_SELF'])) { - message::add($text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //toggle the checked records - if (is_array($records) && @sizeof($records) != 0) { - //get current toggle state - foreach($records as $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - $uuids[] = "'".$record['uuid']."'"; - } - } - if (is_array($uuids) && @sizeof($uuids) != 0) { - $sql = "select ".$this->name."_uuid as uuid, ".$this->toggle_field." as toggle from v_".$this->table." "; - $sql .= "where (domain_uuid = :domain_uuid or domain_uuid is null) "; - $sql .= "and ".$this->name."_uuid in (".implode(', ', $uuids).") "; - $parameters['domain_uuid'] = $this->domain_uuid; - $rows = $this->database->select($sql, $parameters, 'all'); - if (is_array($rows) && @sizeof($rows) != 0) { - foreach ($rows as $row) { - $states[$row['uuid']] = $row['toggle']; - } - } - unset($sql, $parameters, $rows, $row); - } - - //build update array - $x = 0; - foreach($states as $uuid => $state) { - //create the array - $array[$this->table][$x][$this->name.'_uuid'] = $uuid; - $array[$this->table][$x][$this->toggle_field] = $state == $this->toggle_values[0] ? $this->toggle_values[1] : $this->toggle_values[0]; - - //increment the id - $x++; - } - - //save the changes - if (is_array($array) && @sizeof($array) != 0) { - //save the array - $this->database->save($array); - unset($array); - - //set message - message::add($text['message-toggle']); - } - unset($records, $states); - } - } - } - - /** - * copy rows from the database - */ - public function copy($records) { - //assign the variables - $this->name = 'group'; - $this->table = 'groups'; - $this->location = 'groups.php'; - - if (permission_exists($this->name.'_add')) { - - //add multi-lingual support - $language = new text; - $text = $language->get(); - - //validate the token - $token = new token; - if (!$token->validate($_SERVER['PHP_SELF'])) { - message::add($text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //copy the checked records - if (is_array($records) && @sizeof($records) != 0) { - - //get checked records - foreach($records as $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - $uuids[] = "'".$record['uuid']."'"; - } - } - - //create the array from existing data - if (is_array($uuids) && @sizeof($uuids) != 0) { - - //primary table - $sql = "select * from v_".$this->table." "; - $sql .= "where (domain_uuid = :domain_uuid or domain_uuid is null) "; - $sql .= "and ".$this->name."_uuid in (".implode(', ', $uuids).") "; - $parameters['domain_uuid'] = $this->domain_uuid; - $rows = $this->database->select($sql, $parameters, 'all'); - if (is_array($rows) && @sizeof($rows) != 0) { - $y = 0; - foreach ($rows as $x => $row) { - $primary_uuid = uuid(); - - //convert boolean values to a string - foreach($row as $key => $value) { - if (gettype($value) == 'boolean') { - $value = $value ? 'true' : 'false'; - $row[$key] = $value; - } - } - - //copy data - $array[$this->table][$x] = $row; - - //overwrite - $array[$this->table][$x][$this->name.'_uuid'] = $primary_uuid; - $array[$this->table][$x][$this->name.'_description'] = trim($row[$this->name.'_description']).' ('.$text['label-copy'].')'; - - //permissions sub table - $sql_2 = "select * from v_group_permissions where group_uuid = :group_uuid"; - $parameters_2['group_uuid'] = $row['group_uuid']; - $rows_2 = $this->database->select($sql_2, $parameters_2, 'all'); - if (is_array($rows_2) && @sizeof($rows_2) != 0) { - foreach ($rows_2 as $row_2) { - //convert boolean values to a string - foreach($row_2 as $key => $value) { - if (gettype($value) == 'boolean') { - $value = $value ? 'true' : 'false'; - $row_2[$key] = $value; - } - } - - //copy data - $array['group_permissions'][$y] = $row_2; - - //overwrite - $array['group_permissions'][$y]['group_permission_uuid'] = uuid(); - $array['group_permissions'][$y]['group_uuid'] = $primary_uuid; - - //increment - $y++; - - } - } - unset($sql_2, $parameters_2, $rows_2, $row_2); - } - } - unset($sql, $parameters, $rows, $row); - } - - //save the changes and set the message - if (is_array($array) && @sizeof($array) != 0) { - //save the array - $this->database->save($array); - unset($array); - - //set message - message::add($text['message-copy']); - } - unset($records); - } - } - } - - - /** - * add defaults groups - */ - public function defaults() { - - //if the are no groups add the default groups - $sql = "select * from v_groups "; - $sql .= "where domain_uuid is null "; - $result = $this->database->select($sql, null, 'all'); - if (count($result) == 0) { - $x = 0; - $array['groups'][$x]['group_uuid'] = uuid(); - $array['groups'][$x]['domain_uuid'] = null; - $array['groups'][$x]['group_name'] = 'superadmin'; - $array['groups'][$x]['group_level'] = '80'; - $array['groups'][$x]['group_description'] = 'Super Administrator Group'; - $array['groups'][$x]['group_protected'] = 'false'; - $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; - $x++; - $array['groups'][$x]['group_uuid'] = uuid(); - $array['groups'][$x]['domain_uuid'] = null; - $array['groups'][$x]['group_name'] = 'admin'; - $array['groups'][$x]['group_level'] = '50'; - $array['groups'][$x]['group_description'] = 'Administrator Group'; - $array['groups'][$x]['group_protected'] = 'false'; - $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; - $x++; - $array['groups'][$x]['group_uuid'] = uuid(); - $array['groups'][$x]['domain_uuid'] = null; - $array['groups'][$x]['group_name'] = 'user'; - $array['groups'][$x]['group_level'] = '30'; - $array['groups'][$x]['group_description'] = 'User Group'; - $array['groups'][$x]['group_protected'] = 'false'; - $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; - $x++; - $array['groups'][$x]['group_uuid'] = uuid(); - $array['groups'][$x]['domain_uuid'] = null; - $array['groups'][$x]['group_name'] = 'agent'; - $array['groups'][$x]['group_level'] = '20'; - $array['groups'][$x]['group_description'] = 'Call Center Agent Group'; - $array['groups'][$x]['group_protected'] = 'false'; - $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; - $x++; - $array['groups'][$x]['group_uuid'] = uuid(); - $array['groups'][$x]['domain_uuid'] = null; - $array['groups'][$x]['group_name'] = 'fax'; - $array['groups'][$x]['group_level'] = '20'; - $array['groups'][$x]['group_description'] = 'Fax User Group'; - $array['groups'][$x]['group_protected'] = 'false'; - $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; - $x++; - $array['groups'][$x]['group_uuid'] = uuid(); - $array['groups'][$x]['domain_uuid'] = null; - $array['groups'][$x]['group_name'] = 'public'; - $array['groups'][$x]['group_level'] = '10'; - $array['groups'][$x]['group_description'] = 'Public Group'; - $array['groups'][$x]['group_protected'] = 'false'; - $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; - - //add the temporary permissions - $p = permissions::new(); - $p->add("group_add", "temp"); - $p->add("group_edit", "temp"); - - //save the data to the database - $this->database->save($array); - unset($array); - - //remove the temporary permission - $p->delete("group_add", "temp"); - $p->delete("group_edit", "temp"); - } - unset($result); - - //if there are no permissions listed in v_group_permissions then set the default permissions - $sql = "select count(*) from v_group_permissions "; - $sql .= "where domain_uuid is null "; - $num_rows = $this->database->select($sql, null, 'column'); - if ($num_rows == 0) { - //build the apps array - $config_list = glob($_SERVER["DOCUMENT_ROOT"].PROJECT_PATH."/*/*/app_config.php"); - $x = 0; - foreach ($config_list as $config_path) { - include($config_path); - $x++; - } - - //no permissions found add the defaults - foreach ($apps as $app) { - if (is_array($app['permissions'])) foreach ($app['permissions'] as $row) { - if (is_array($row['groups'])) foreach ($row['groups'] as $group) { - $x++; - $array['group_permissions'][$x]['group_permission_uuid'] = uuid(); - $array['group_permissions'][$x]['domain_uuid'] = null; - $array['group_permissions'][$x]['permission_name'] = $row['name']; - $array['group_permissions'][$x]['permission_protected'] = 'false'; - $array['group_permissions'][$x]['permission_assigned'] = 'true'; - $array['group_permissions'][$x]['group_name'] = $group; - $array['group_permissions'][$x]['group_uuid'] = $group_uuids[$group]; - } - } - } - unset($group_uuids); - - //add the temporary permissions - $p = permissions::new(); - $p->add("group_permission_add", "temp"); - $p->add("group_permission_edit", "temp"); - - //save the data to the database - $this->database->save($array); - unset($array); - - //remove the temporary permission - $p->delete("group_permission_add", "temp"); - $p->delete("group_permission_edit", "temp"); - } - } - - /** - * get the groups assigned to the user - */ - public function assigned() { - $sql = "select "; - $sql .= "u.user_group_uuid, "; - $sql .= "u.domain_uuid, "; - $sql .= "u.user_uuid, "; - $sql .= "u.group_uuid, "; - $sql .= "g.group_name, "; - $sql .= "g.group_level "; - $sql .= "from "; - $sql .= "v_user_groups as u, "; - $sql .= "v_groups as g "; - $sql .= "where u.domain_uuid = :domain_uuid "; - $sql .= "and u.user_uuid = :user_uuid "; - $sql .= "and u.group_uuid = g.group_uuid "; - $parameters['domain_uuid'] = $this->domain_uuid; - $parameters['user_uuid'] = $this->user_uuid; - $groups = $this->database->select($sql, $parameters, 'all'); - unset($sql, $parameters); - if (!empty($groups)) { - return $groups; - } - else { - return []; - } - } - - /** - * add the assigned groups to the session array - */ - public function session() { - $_SESSION["groups"] = $this->groups; - $_SESSION["user"]["groups"] = $this->groups; - $_SESSION["user"]["group_level"] = $this->group_level; - } } + + /** + * Retrieves assigned user groups for a given user. + * + * This method executes a SQL query to retrieve the assigned user groups + * for the specified domain and user. The results are returned as an array. + * + * @return array|null An array of assigned user groups, or null if no groups are assigned. + */ + public function assigned() { + $sql = "select "; + $sql .= "u.user_group_uuid, "; + $sql .= "u.domain_uuid, "; + $sql .= "u.user_uuid, "; + $sql .= "u.group_uuid, "; + $sql .= "g.group_name, "; + $sql .= "g.group_level "; + $sql .= "from "; + $sql .= "v_user_groups as u, "; + $sql .= "v_groups as g "; + $sql .= "where u.domain_uuid = :domain_uuid "; + $sql .= "and u.user_uuid = :user_uuid "; + $sql .= "and u.group_uuid = g.group_uuid "; + $parameters['domain_uuid'] = $this->domain_uuid; + $parameters['user_uuid'] = $this->user_uuid; + $groups = $this->database->select($sql, $parameters, 'all'); + unset($sql, $parameters); + if (!empty($groups)) { + return $groups; + } else { + return []; + } + } + + /** + * Get the groups for the current context. + * + * @return array An array of group information. + */ + public function get_groups() { + //return the groups + return $this->groups; + } + + /** + * Delete multiple group member records. + * + * @param array $records Array of checked records to delete. + * + * @return void + */ + public function delete_members($records) { + //assign the variables + $this->name = 'group_member'; + $this->table = 'user_groups'; + $this->location = 'group_members.php?group_uuid=' . $this->group_uuid; + + if (permission_exists($this->name . '_delete')) { + + //add multi-lingual support + $language = new text; + $text = $language->get(); + + //validate the token + $token = new token; + if (!$token->validate($_SERVER['PHP_SELF'])) { + message::add($text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; + } + + //delete multiple records + if (is_array($records) && @sizeof($records) != 0) { + //build array of checked records + foreach ($records as $x => $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + $array[$this->table][$x]['user_uuid'] = $record['uuid']; + $array[$this->table][$x]['group_uuid'] = $this->group_uuid; + } + } + + //delete the checked rows + if (is_array($array) && @sizeof($array) != 0) { + + //grant temporary permissions + $p = permissions::new(); + $p->add('user_group_delete', 'temp'); + + //execute delete + $this->database->delete($array); + unset($array); + + //revoke temporary permissions + $p->delete('user_group_delete', 'temp'); + + //set message + message::add($text['message-delete']); + } + unset($records); + } + } + } + + /** + * Deletes one or multiple records. + * + * @param array $records An array of record IDs to delete, where each ID is an associative array + * containing 'uuid' and 'checked' keys. The 'checked' value indicates + * whether the corresponding checkbox was checked for deletion. + * + * @return void No return value; this method modifies the database state and sets a message. + */ + public function delete($records) { + //assign the variables + $this->name = 'group'; + $this->table = 'groups'; + $this->location = 'groups.php'; + + if (permission_exists($this->name . '_delete')) { + + //add multi-lingual support + $language = new text; + $text = $language->get(); + + //validate the token + $token = new token; + if (!$token->validate($_SERVER['PHP_SELF'])) { + message::add($text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; + } + + //delete multiple records + if (is_array($records) && @sizeof($records) != 0) { + //build array of checked records + foreach ($records as $x => $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + $array[$this->table][$x][$this->name . '_uuid'] = $record['uuid']; + $array['group_permissions'][$x][$this->name . '_uuid'] = $record['uuid']; + } + } + + //delete the checked rows + if (is_array($array) && @sizeof($array) != 0) { + + //grant temporary permissions + $p = permissions::new(); + $p->add('group_permission_delete', 'temp'); + + //execute delete + $this->database->delete($array); + unset($array); + + //revoke temporary permissions + $p->delete('group_permission_delete', 'temp'); + + //set message + message::add($text['message-delete']); + } + unset($records); + } + } + } + + /** + * Toggles the state of one or more records. + * + * @param array $records An array of record IDs to delete, where each ID is an associative array + * containing 'uuid' and 'checked' keys. The 'checked' value indicates + * whether the corresponding checkbox was checked for deletion. + * + * @return void No return value; this method modifies the database state and sets a message. + */ + public function toggle($records) { + //assign the variables + $this->name = 'group'; + $this->table = 'groups'; + $this->toggle_field = 'group_protected'; + $this->toggle_values = ['true', 'false']; + $this->location = 'groups.php'; + + if (permission_exists($this->name . '_edit')) { + + //add multi-lingual support + $language = new text; + $text = $language->get(); + + //validate the token + $token = new token; + if (!$token->validate($_SERVER['PHP_SELF'])) { + message::add($text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; + } + + //toggle the checked records + if (is_array($records) && @sizeof($records) != 0) { + //get current toggle state + foreach ($records as $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + $uuids[] = "'" . $record['uuid'] . "'"; + } + } + if (is_array($uuids) && @sizeof($uuids) != 0) { + $sql = "select " . $this->name . "_uuid as uuid, " . $this->toggle_field . " as toggle from v_" . $this->table . " "; + $sql .= "where (domain_uuid = :domain_uuid or domain_uuid is null) "; + $sql .= "and " . $this->name . "_uuid in (" . implode(', ', $uuids) . ") "; + $parameters['domain_uuid'] = $this->domain_uuid; + $rows = $this->database->select($sql, $parameters, 'all'); + if (is_array($rows) && @sizeof($rows) != 0) { + foreach ($rows as $row) { + $states[$row['uuid']] = $row['toggle']; + } + } + unset($sql, $parameters, $rows, $row); + } + + //build update array + $x = 0; + foreach ($states as $uuid => $state) { + //create the array + $array[$this->table][$x][$this->name . '_uuid'] = $uuid; + $array[$this->table][$x][$this->toggle_field] = $state == $this->toggle_values[0] ? $this->toggle_values[1] : $this->toggle_values[0]; + + //increment the id + $x++; + } + + //save the changes + if (is_array($array) && @sizeof($array) != 0) { + //save the array + $this->database->save($array); + unset($array); + + //set message + message::add($text['message-toggle']); + } + unset($records, $states); + } + } + } + + /** + * Copies one or more records + * + * @param array $records An array of record IDs to delete, where each ID is an associative array + * containing 'uuid' and 'checked' keys. The 'checked' value indicates + * whether the corresponding checkbox was checked for deletion. + * + * @return void No return value; this method modifies the database state and sets a message. + */ + public function copy($records) { + //assign the variables + $this->name = 'group'; + $this->table = 'groups'; + $this->location = 'groups.php'; + + if (permission_exists($this->name . '_add')) { + + //add multi-lingual support + $language = new text; + $text = $language->get(); + + //validate the token + $token = new token; + if (!$token->validate($_SERVER['PHP_SELF'])) { + message::add($text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; + } + + //copy the checked records + if (is_array($records) && @sizeof($records) != 0) { + + //get checked records + foreach ($records as $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + $uuids[] = "'" . $record['uuid'] . "'"; + } + } + + //create the array from existing data + if (is_array($uuids) && @sizeof($uuids) != 0) { + + //primary table + $sql = "select * from v_" . $this->table . " "; + $sql .= "where (domain_uuid = :domain_uuid or domain_uuid is null) "; + $sql .= "and " . $this->name . "_uuid in (" . implode(', ', $uuids) . ") "; + $parameters['domain_uuid'] = $this->domain_uuid; + $rows = $this->database->select($sql, $parameters, 'all'); + if (is_array($rows) && @sizeof($rows) != 0) { + $y = 0; + foreach ($rows as $x => $row) { + $primary_uuid = uuid(); + + //convert boolean values to a string + foreach ($row as $key => $value) { + if (gettype($value) == 'boolean') { + $value = $value ? 'true' : 'false'; + $row[$key] = $value; + } + } + + //copy data + $array[$this->table][$x] = $row; + + //overwrite + $array[$this->table][$x][$this->name . '_uuid'] = $primary_uuid; + $array[$this->table][$x][$this->name . '_description'] = trim($row[$this->name . '_description']) . ' (' . $text['label-copy'] . ')'; + + //permissions sub table + $sql_2 = "select * from v_group_permissions where group_uuid = :group_uuid"; + $parameters_2['group_uuid'] = $row['group_uuid']; + $rows_2 = $this->database->select($sql_2, $parameters_2, 'all'); + if (is_array($rows_2) && @sizeof($rows_2) != 0) { + foreach ($rows_2 as $row_2) { + //convert boolean values to a string + foreach ($row_2 as $key => $value) { + if (gettype($value) == 'boolean') { + $value = $value ? 'true' : 'false'; + $row_2[$key] = $value; + } + } + + //copy data + $array['group_permissions'][$y] = $row_2; + + //overwrite + $array['group_permissions'][$y]['group_permission_uuid'] = uuid(); + $array['group_permissions'][$y]['group_uuid'] = $primary_uuid; + + //increment + $y++; + + } + } + unset($sql_2, $parameters_2, $rows_2, $row_2); + } + } + unset($sql, $parameters, $rows, $row); + } + + //save the changes and set the message + if (is_array($array) && @sizeof($array) != 0) { + //save the array + $this->database->save($array); + unset($array); + + //set message + message::add($text['message-copy']); + } + unset($records); + } + } + } + + /** + * Set default groups and permissions if none are set. + * + * This method checks for the presence of groups and group permissions in the database. + * If no groups or group permissions exist, it sets default values. + */ + public function defaults() { + + //if the are no groups add the default groups + $sql = "select * from v_groups "; + $sql .= "where domain_uuid is null "; + $result = $this->database->select($sql, null, 'all'); + if (count($result) == 0) { + $x = 0; + $array['groups'][$x]['group_uuid'] = uuid(); + $array['groups'][$x]['domain_uuid'] = null; + $array['groups'][$x]['group_name'] = 'superadmin'; + $array['groups'][$x]['group_level'] = '80'; + $array['groups'][$x]['group_description'] = 'Super Administrator Group'; + $array['groups'][$x]['group_protected'] = 'false'; + $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; + $x++; + $array['groups'][$x]['group_uuid'] = uuid(); + $array['groups'][$x]['domain_uuid'] = null; + $array['groups'][$x]['group_name'] = 'admin'; + $array['groups'][$x]['group_level'] = '50'; + $array['groups'][$x]['group_description'] = 'Administrator Group'; + $array['groups'][$x]['group_protected'] = 'false'; + $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; + $x++; + $array['groups'][$x]['group_uuid'] = uuid(); + $array['groups'][$x]['domain_uuid'] = null; + $array['groups'][$x]['group_name'] = 'user'; + $array['groups'][$x]['group_level'] = '30'; + $array['groups'][$x]['group_description'] = 'User Group'; + $array['groups'][$x]['group_protected'] = 'false'; + $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; + $x++; + $array['groups'][$x]['group_uuid'] = uuid(); + $array['groups'][$x]['domain_uuid'] = null; + $array['groups'][$x]['group_name'] = 'agent'; + $array['groups'][$x]['group_level'] = '20'; + $array['groups'][$x]['group_description'] = 'Call Center Agent Group'; + $array['groups'][$x]['group_protected'] = 'false'; + $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; + $x++; + $array['groups'][$x]['group_uuid'] = uuid(); + $array['groups'][$x]['domain_uuid'] = null; + $array['groups'][$x]['group_name'] = 'fax'; + $array['groups'][$x]['group_level'] = '20'; + $array['groups'][$x]['group_description'] = 'Fax User Group'; + $array['groups'][$x]['group_protected'] = 'false'; + $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; + $x++; + $array['groups'][$x]['group_uuid'] = uuid(); + $array['groups'][$x]['domain_uuid'] = null; + $array['groups'][$x]['group_name'] = 'public'; + $array['groups'][$x]['group_level'] = '10'; + $array['groups'][$x]['group_description'] = 'Public Group'; + $array['groups'][$x]['group_protected'] = 'false'; + $group_uuids[$array['groups'][$x]['group_name']] = $array['groups'][$x]['group_uuid']; + + //add the temporary permissions + $p = permissions::new(); + $p->add("group_add", "temp"); + $p->add("group_edit", "temp"); + + //save the data to the database + $this->database->save($array); + unset($array); + + //remove the temporary permission + $p->delete("group_add", "temp"); + $p->delete("group_edit", "temp"); + } + unset($result); + + //if there are no permissions listed in v_group_permissions then set the default permissions + $sql = "select count(*) from v_group_permissions "; + $sql .= "where domain_uuid is null "; + $num_rows = $this->database->select($sql, null, 'column'); + if ($num_rows == 0) { + //build the apps array + $config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php"); + $x = 0; + foreach ($config_list as $config_path) { + include($config_path); + $x++; + } + + //no permissions found add the defaults + foreach ($apps as $app) { + if (is_array($app['permissions'])) foreach ($app['permissions'] as $row) { + if (is_array($row['groups'])) foreach ($row['groups'] as $group) { + $x++; + $array['group_permissions'][$x]['group_permission_uuid'] = uuid(); + $array['group_permissions'][$x]['domain_uuid'] = null; + $array['group_permissions'][$x]['permission_name'] = $row['name']; + $array['group_permissions'][$x]['permission_protected'] = 'false'; + $array['group_permissions'][$x]['permission_assigned'] = 'true'; + $array['group_permissions'][$x]['group_name'] = $group; + $array['group_permissions'][$x]['group_uuid'] = $group_uuids[$group]; + } + } + } + unset($group_uuids); + + //add the temporary permissions + $p = permissions::new(); + $p->add("group_permission_add", "temp"); + $p->add("group_permission_edit", "temp"); + + //save the data to the database + $this->database->save($array); + unset($array); + + //remove the temporary permission + $p->delete("group_permission_add", "temp"); + $p->delete("group_permission_edit", "temp"); + } + } + + /** + * Stores user data and groups in the session. + * + * This method populates several session variables with relevant user and group information. + * + * @return void + */ + public function session() { + $_SESSION["groups"] = $this->groups; + $_SESSION["user"]["groups"] = $this->groups; + $_SESSION["user"]["group_level"] = $this->group_level; + } +} diff --git a/resources/classes/invalid_uuid_exception.php b/resources/classes/invalid_uuid_exception.php index a54e6190ba..1b01bbad77 100644 --- a/resources/classes/invalid_uuid_exception.php +++ b/resources/classes/invalid_uuid_exception.php @@ -32,7 +32,16 @@ * @author Tim Fry */ class invalid_uuid_exception extends Exception { - public function __construct(string $message = "UUID is not valid", int $code = 0, ?\Throwable $previous = null) { - return parent::__construct($message, $code, $previous); + /** + * Constructs a new instance of the class. + * + * @param string $message The error message. Defaults to "UUID is not valid". + * @param int $code The HTTP status code. Defaults to 0. + * @param \Throwable|null $previous The previous exception, if any. + * + * @return void + */ + public function __construct(string $message = "UUID is not valid", int $code = 0, ?Throwable $previous = null) { + parent::__construct($message, $code, $previous); } } diff --git a/resources/classes/logging.php b/resources/classes/logging.php index 2b1a4ff897..f696fbdb63 100644 --- a/resources/classes/logging.php +++ b/resources/classes/logging.php @@ -1,165 +1,269 @@ clear_debug(); - // declare log file and file pointer as private properties - private $fp; - private $debug_func; - private $debug_line; - private $debug_file; - private $debug_class; - - public function __construct(string $filename_and_path) { - //init values - $this->clear_debug(); - - try { - //open file in append mode - $this->fp = fopen($filename_and_path, 'a'); - } catch (Exception $ex) { - //send the error to the caller - throw $ex; - } + try { + //open file in append mode + $this->fp = fopen($filename_and_path, 'a'); + } catch (Exception $ex) { + //send the error to the caller + throw $ex; } + } - public function __destruct() { - try { - $this->flush(); - } catch (Exception $ex) { - //do nothing - } finally { - //close the file - if (is_resource($this->fp)) { - fclose($this->fp); - } - } - } + /** + * Clear debug settings + * + * @return void + */ + private function clear_debug() { + $this->debug_line = null; + $this->debug_file = null; + $this->debug_func = null; + $this->debug_class = null; + } - /** - * Ensure all data arrives on disk - * @throws Exception - */ - public function flush() { - try { - //ensure everything arrives on disk - if (is_resource($this->fp)) { - fflush($this->fp); - } - } catch (Exception $ex) { - throw $ex; - } - } - - // write message to the log file - private function _write($msg) { - // define current time and suppress E_WARNING if using the system TZ settings - } - - private function clear_debug() { - $this->debug_line = null; - $this->debug_file = null; - $this->debug_func = null; - $this->debug_class = null; - } - - /** - * Write raw data to the - * @param string $level - * @param string $message - */ - public function write(string $level, string $message) { - $this->get_backtrace_details(); - // write current time, script name and message to the log file - // (don't forget to set the INI setting date.timezone) - $time = @date('Y-m-d H:i:s'); - $file = $this->debug_file ?? 'file not set'; - $line = $this->debug_line ?? '0000'; - fwrite($this->fp, "[$time] [$level] [{$file}:{$line}] $message"); - $this->clear_debug(); - } - - public function debug_class(?string $debug_class = null) { - if (func_num_args() > 0) { - $this->debug_class = $debug_class; - return $this; - } - return $this->debug_class; - } - - public function debug_line(?string $debug_line = null) { - if (func_num_args() > 0) { - $this->debug_line = $debug_line; - return $this; - } - return $this->debug_line; - } - - public function debug_func(?string $debug_func = null) { - if (func_num_args() > 0) { - $this->debug_func = $debug_func; - return $this; - } - return $this->debug_func; - } - - public function debug_file(?string $debug_file = null) { - if (func_num_args() > 0) { - $this->debug_file = $debug_file; - return $this; - } - return $this->debug_file; - } - - public function writeln($level, $message) { - $this->get_backtrace_details(); - $this->write($level, $message . "\n"); - } - - public function debug($message) { - $this->get_backtrace_details(); - $this->writeln("DEBUG", $message); - } - - public function info($message) { - $this->get_backtrace_details(); - $this->writeln("INFO", $message); - } - - public function warning($message) { - $this->get_backtrace_details(); - $this->writeln("WARNING", $message); - } - - public function error($message) { - $this->get_backtrace_details(); - $this->writeln("ERROR", $message); - } - - private function get_backtrace_details() { - if ($this->debug_file === null) { - $debug = debug_backtrace(); - $ndx = count($debug) - 1; - $this->debug_file = $debug[$ndx]['file']; - $this->debug_line = $debug[$ndx]['line']; - $this->debug_func = $debug[$ndx]['function']; - $this->debug_class = $debug[$ndx]['class'] ?? ''; + /** + * Clean up any resources held by this object on destruction. + * + * @throws Exception if an error occurs during flushing or closing of the file pointer. + */ + public function __destruct() { + try { + $this->flush(); + } catch (Exception $ex) { + //do nothing + } finally { + //close the file + if (is_resource($this->fp)) { + fclose($this->fp); } } } - /* - * Example: - $log = new logging(sys_get_temp_dir() . '/logging.log'); - $log->writeln("debug", "passed validation"); - $log->debug("pass"); - $log->warning("variable should not used"); - $log->debug_file(__FILE__)->debug_line(__LINE__)->write("DEBUG", "Raw message\n"); - */ \ No newline at end of file + // write message to the log file + + /** + * Ensure all data arrives on disk + * + * @return void + * @throws Exception + */ + public function flush() { + try { + //ensure everything arrives on disk + if (is_resource($this->fp)) { + fflush($this->fp); + } + } catch (Exception $ex) { + throw $ex; + } + } + + /** + * Set or retrieve the class name for debugging purposes. + * + * @param string|null $debug_class The class name to set for debugging. If null, returns the current debug class. + * + * @return object This object instance for chaining. + */ + public function debug_class(?string $debug_class = null) { + if (func_num_args() > 0) { + $this->debug_class = $debug_class; + return $this; + } + return $this->debug_class; + } + + /** + * Set or retrieve the current debug line + * + * @param string|null $debug_line The new debug line to set, or null to clear it + * + * @return object|string The instance itself if a new value was provided, otherwise the current debug line as a + * string + */ + public function debug_line(?string $debug_line = null) { + if (func_num_args() > 0) { + $this->debug_line = $debug_line; + return $this; + } + return $this->debug_line; + } + + /** + * Sets or retrieves the current debug function. + * + * If a string argument is provided, it sets the current debug function. If no argument is provided, + * it returns the current debug function. + * + * @param string|null $debug_func The debug function to set (optional) + * + * @return $this Self-reference for chaining + */ + public function debug_func(?string $debug_func = null) { + if (func_num_args() > 0) { + $this->debug_func = $debug_func; + return $this; + } + return $this->debug_func; + } + + /** + * Set or retrieve the path to a debug file. + * + * @param string|null $debug_file Path to the debug file (optional) + * + * @return object|string The current object if setting the debug file, otherwise the current debug file path + */ + public function debug_file(?string $debug_file = null) { + if (func_num_args() > 0) { + $this->debug_file = $debug_file; + return $this; + } + return $this->debug_file; + } + + /** + * Write a debug message to the log along with its backtrace details + * + * @param string $message The debug message to be written + * + * @return void + */ + public function debug($message) { + $this->get_backtrace_details(); + $this->writeln("DEBUG", $message); + } + + /** + * Get detailed backtrace information for the current call stack. + * + * If the debug file, line and function have not been cached, this method will + * cache them in object properties to prevent repeated calls to debug_backtrace(). + * + * @return void + */ + private function get_backtrace_details() { + if ($this->debug_file === null) { + $debug = debug_backtrace(); + $ndx = count($debug) - 1; + $this->debug_file = $debug[$ndx]['file']; + $this->debug_line = $debug[$ndx]['line']; + $this->debug_func = $debug[$ndx]['function']; + $this->debug_class = $debug[$ndx]['class'] ?? ''; + } + } + + /** + * Write a message to the output with an optional level and trailing newline character. + * + * @param string $level The logging level (optional). + * @param string $message The message to be written. + */ + public function writeln($level, $message) { + $this->get_backtrace_details(); + $this->write($level, $message . "\n"); + } + + /** + * Write a log message to the file + * + * @param string $level The level of the log message (e.g. 'error', 'warning', etc.) + * @param string $message The actual log message + * + * @return void + */ + public function write(string $level, string $message) { + $this->get_backtrace_details(); + // write current time, script name and message to the log file + // (don't forget to set the INI setting date.timezone) + $time = @date('Y-m-d H:i:s'); + $file = $this->debug_file ?? 'file not set'; + $line = $this->debug_line ?? '0000'; + fwrite($this->fp, "[$time] [$level] [{$file}:{$line}] $message"); + $this->clear_debug(); + } + + /** + * Log an informational message. + * + * This method logs a message with level "INFO" and stores backtrace details for debugging purposes. + * + * @param string $message The message to be logged + */ + public function info($message) { + $this->get_backtrace_details(); + $this->writeln("INFO", $message); + } + + /** + * Display a warning message to the user + * + * @param string $message The warning message to display + * + * @throws Exception If an error occurs while displaying the message + */ + public function warning($message) { + $this->get_backtrace_details(); + $this->writeln("WARNING", $message); + } + + /** + * Log an error message with a backtrace. + * + * @param string $message The error message to log + */ + public function error($message) { + $this->get_backtrace_details(); + $this->writeln("ERROR", $message); + } + + /** + * Writes a message to the underlying output stream. + * + * @param string $msg The message to be written + * + * @throws Exception If an error occurs while writing to the stream + */ + private function _write($msg) { + // define current time and suppress E_WARNING if using the system TZ settings + } +} + +/* +* Example: +$log = new logging(sys_get_temp_dir() . '/logging.log'); +$log->writeln("debug", "passed validation"); +$log->debug("pass"); +$log->warning("variable should not used"); +$log->debug_file(__FILE__)->debug_line(__LINE__)->write("DEBUG", "Raw message\n"); +*/ \ No newline at end of file diff --git a/resources/classes/menu.php b/resources/classes/menu.php index 47dde80697..483a498782 100644 --- a/resources/classes/menu.php +++ b/resources/classes/menu.php @@ -27,1485 +27,1553 @@ /** * menu class */ - class menu { +class menu { - /** - * declare constant variables - */ - const app_name = 'menus'; - const app_uuid = 'f4b3b3d2-6287-489c-2a00-64529e46f2d7'; + /** + * declare constant variables + */ + const app_name = 'menus'; + const app_uuid = 'f4b3b3d2-6287-489c-2a00-64529e46f2d7'; - /** - * declare private variables - */ - public $menu_uuid; - public $menu_language; - public $text; + /** + * declare private variables + */ + public $menu_uuid; + public $menu_language; + public $text; - /** - * declare private variables - */ - private $name; - private $table; - private $toggle_field; - private $toggle_values; - private $location; + /** + * declare private variables + */ + private $name; + private $table; + private $toggle_field; + private $toggle_values; + private $location; - /** - * Set in the constructor. Must be a database object and cannot be null. - * @var database Database Object - */ - private $database; + /** + * Set in the constructor. Must be a database object and cannot be null. + * + * @var database Database Object + */ + private $database; - /** - * Settings object set in the constructor. Must be a settings object and cannot be null. - * @var settings Settings Object - */ - private $settings; + /** + * Settings object set in the constructor. Must be a settings object and cannot be null. + * + * @var settings Settings Object + */ + private $settings; - /** - * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array - * @var string - */ - private $user_uuid; + /** + * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in + * the session global array + * + * @var string + */ + private $user_uuid; - /** - * Username set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array - * @var string - */ - private $username; + /** + * Username set in the constructor. This can be passed in through the $settings_array associative array or set in + * the session global array + * + * @var string + */ + private $username; - /** - * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array - * @var string - */ - private $domain_uuid; + /** + * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set + * in the session global array + * + * @var string + */ + private $domain_uuid; - /** - * Domain name set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array - * @var string - */ - private $domain_name; + /** + * Domain name set in the constructor. This can be passed in through the $settings_array associative array or set + * in the session global array + * + * @var string + */ + private $domain_name; - /** - * called when the object is created - */ - public function __construct($setting_array = []) { - //assign the variables - $this->location = 'menus.php'; + /** + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $setting_array An optional array of settings to override default values. Defaults to []. + */ + public function __construct($setting_array = []) { + //assign the variables + $this->location = 'menus.php'; - $this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? ''; - $this->domain_name = $setting_array['domain_name'] ?? $_SESSION['domain_name'] ?? ''; - $this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? ''; - $this->username = $setting_array['username'] ?? $_SESSION['username'] ?? ''; + $this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? ''; + $this->domain_name = $setting_array['domain_name'] ?? $_SESSION['domain_name'] ?? ''; + $this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? ''; + $this->username = $setting_array['username'] ?? $_SESSION['username'] ?? ''; - //open a database connection - $this->database = $setting_array['database'] ?? database::new(); + //open a database connection + $this->database = $setting_array['database'] ?? database::new(); - //load the settings - $this->settings = $setting_array['settings'] ?? new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]); + //load the settings + $this->settings = $setting_array['settings'] ?? new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]); - //add multi-lingual support - $this->text = (new text)->get(); - } + //add multi-lingual support + $this->text = (new text)->get(); + } - /** - * delete rows from the database - */ - public function delete($records) { - //assign the variables - $this->name = 'menu'; - $this->table = 'menus'; + /** + * Deletes one or more menu items. + * + * @param array $records An array of records to delete, where each record contains a 'uuid' key with the UUID of + * the item to delete, and an optional 'checked' key with a boolean value indicating whether + * the item is checked for deletion. + */ + public function delete_items($records) { + //assign the variables + $this->name = 'menu_item'; + $this->table = 'menu_items'; - if (permission_exists($this->name.'_delete')) { + if (permission_exists($this->name . '_delete')) { - //validate the token - $token = new token; - if (!$token->validate($_SERVER['PHP_SELF'])) { - message::add($this->text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //delete multiple records - if (is_array($records) && @sizeof($records) != 0) { - //build the delete array - $x = 0; - foreach ($records as $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - //remove menu languages - $array['menu_languages'][$x][$this->name.'_uuid'] = $record['uuid']; - - //remove menu item groups - $array['menu_item_groups'][$x][$this->name.'_uuid'] = $record['uuid']; - - //remove menu items - $array['menu_items'][$x][$this->name.'_uuid'] = $record['uuid']; - - //build array to remove the menu - $array[$this->table][$x][$this->name.'_uuid'] = $record['uuid']; - - //increment - $x++; - } - } - - //delete the checked rows - if (is_array($array) && @sizeof($array) != 0) { - //grant temporary permissions - $p = permissions::new(); - $p->add('menu_item_delete', 'temp'); - $p->add('menu_item_group_delete', 'temp'); - $p->add('menu_language_delete', 'temp'); - - //execute delete - $this->database->delete($array); - unset($array); - - //revoke temporary permissions - $p->delete('menu_item_delete', 'temp'); - $p->delete('menu_item_group_delete', 'temp'); - $p->delete('menu_language_delete', 'temp'); - - //set message - message::add($this->text['message-delete']); - } - unset($records); - } + //validate the token + $token = new token; + if (!$token->validate('/core/menu/menu_item_list.php')) { + message::add($this->text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; } - } - public function delete_items($records) { - //assign the variables - $this->name = 'menu_item'; - $this->table = 'menu_items'; - - if (permission_exists($this->name.'_delete')) { - - //validate the token - $token = new token; - if (!$token->validate('/core/menu/menu_item_list.php')) { - message::add($this->text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //delete multiple records - if (is_array($records) && @sizeof($records) != 0) { - //build the delete array - $x = 0; - foreach ($records as $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - //build array - $uuids[] = "'".$record['uuid']."'"; - //remove menu languages - $array['menu_languages'][$x][$this->name.'_uuid'] = $record['uuid']; - //remove menu item groups - $array['menu_item_groups'][$x][$this->name.'_uuid'] = $record['uuid']; - //remove menu items - $array[$this->table][$x][$this->name.'_uuid'] = $record['uuid']; - //increment - $x++; - } - } - - //include child menu items - if (!empty($uuids) && @sizeof($uuids) != 0) { - $sql = "select menu_item_uuid as uuid from v_".$this->table." "; - $sql .= "where menu_item_parent_uuid in (".implode(', ', $uuids).") "; - $rows = $this->database->select($sql, null, 'all'); - if (!empty($rows) && @sizeof($rows) != 0) { - foreach ($rows as $row) { - //remove menu languages - $array['menu_languages'][$x][$this->name.'_uuid'] = $row['uuid']; - //remove menu item groups - $array['menu_item_groups'][$x][$this->name.'_uuid'] = $row['uuid']; - //remove menu items - $array[$this->table][$x][$this->name.'_uuid'] = $row['uuid']; - //increment - $x++; - } - } - } - - //delete the checked rows - if (!empty($array) && is_array($array) && @sizeof($array) != 0) { - - //grant temporary permissions - $p = permissions::new(); - $p->add('menu_language_delete', 'temp'); - $p->add('menu_item_group_delete', 'temp'); - - //execute delete - $this->database->delete($array); - unset($array); - - //revoke temporary permissions - $p->delete('menu_language_delete', 'temp'); - $p->delete('menu_item_group_delete', 'temp'); - - //set message - message::add($this->text['message-delete']); - } - unset($records); - } - } - } - - /** - * toggle a field between two values - */ - public function toggle_items($records) { - //assign the variables - $this->name = 'menu_item'; - $this->table = 'menu_items'; - $this->toggle_field = 'menu_item_protected'; - $this->toggle_values = ['true','false']; - - if (permission_exists($this->name.'_edit')) { - - //validate the token - $token = new token; - if (!$token->validate('/core/menu/menu_item_list.php')) { - message::add($this->text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //toggle the checked records - if (is_array($records) && @sizeof($records) != 0) { - //get current toggle state - foreach ($records as $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - $uuids[] = "'".$record['uuid']."'"; - } - } - if (!empty($uuids) && is_array($uuids) && @sizeof($uuids) != 0) { - $sql = "select ".$this->name."_uuid as uuid, ".$this->toggle_field." as toggle from v_".$this->table." "; - $sql .= "where ".$this->name."_uuid in (".implode(', ', $uuids).") "; - $parameters = null; - $rows = $this->database->select($sql, $parameters, 'all'); - if (is_array($rows) && @sizeof($rows) != 0) { - foreach ($rows as $row) { - $states[$row['uuid']] = $row['toggle'] == '' ? $this->toggle_values[1] : $row['toggle']; - } - } - unset($sql, $parameters, $rows, $row); - } - - //build update array - $x = 0; - if (!empty($states) && is_array($states) && @sizeof($states) != 0) { - foreach ($states as $uuid => $state) { - //create the array - $array[$this->table][$x][$this->name.'_uuid'] = $uuid; - $array[$this->table][$x][$this->toggle_field] = $state == $this->toggle_values[0] ? $this->toggle_values[1] : $this->toggle_values[0]; - - //increment - $x++; - } - } - - //save the changes - if (!empty($array) && is_array($array) && @sizeof($array) != 0) { - //save the array - $this->database->save($array); - unset($array); - - //set message - message::add($this->text['message-toggle']); - } - unset($records, $states); - } - } - } - - /** - * delete items in the menu used by restore default - */ - public function restore_delete() { - //remove existing menu languages - $sql = "delete from v_menu_languages "; - $sql .= "where menu_uuid = :menu_uuid "; - $sql .= "and menu_item_uuid in ( "; - $sql .= " select menu_item_uuid "; - $sql .= " from v_menu_items "; - $sql .= " where menu_uuid = :menu_uuid "; - //$sql .= " and ( "; - //$sql .= " menu_item_protected <> 'true' "; - //$sql .= " or menu_item_protected is null "; - //$sql .= " ) "; - $sql .= ") "; - $parameters['menu_uuid'] = $this->menu_uuid; - $this->database->execute($sql, $parameters); - unset($sql, $parameters); - - //remove existing menu item groups - $sql = "delete from v_menu_item_groups "; - $sql .= "where menu_uuid = :menu_uuid "; - $sql .= "and menu_item_uuid in ( "; - $sql .= " select menu_item_uuid "; - $sql .= " from v_menu_items "; - $sql .= " where menu_uuid = :menu_uuid "; - //$sql .= " and ( "; - //$sql .= " menu_item_protected <> 'true' "; - //$sql .= " or menu_item_protected is null "; - //$sql .= " ) "; - $sql .= ") "; - $parameters['menu_uuid'] = $this->menu_uuid; - $this->database->execute($sql, $parameters); - unset($sql, $parameters); - - //remove existing menu items - $sql = "delete from v_menu_items "; - $sql .= "where menu_uuid = :menu_uuid "; - //$sql .= "and ( "; - //$sql .= " menu_item_protected <> 'true' "; - //$sql .= " or menu_item_protected is null "; - //$sql .= ") "; - $parameters['menu_uuid'] = $this->menu_uuid; - $this->database->execute($sql, $parameters); - unset($sql, $parameters); - } - - public function assign_items($records, $menu_uuid, $group_uuid) { - //assign the variables - $this->name = 'menu_item'; - $this->table = 'menu_items'; - - if (permission_exists($this->name.'_add')) { - - //add multi-lingual support - $language = new text; - $text = $language->get(); - - //validate the token - $token = new token; - if (!$token->validate('/core/menu/menu_item_list.php')) { - message::add($text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //assign multiple records - if (is_array($records) && @sizeof($records) != 0 && !empty($group_uuid)) { - - //define the group_name, group_uuid, menu_uuid - if (!empty($records) && @sizeof($records) != 0) { - $sql = "select group_name, group_uuid from v_groups "; - $sql .= "where group_uuid = :group_uuid "; - $parameters['group_uuid'] = $group_uuid; - $group = $this->database->select($sql, $parameters, 'row'); - } - - //build the delete array - $x = 0; - foreach ($records as $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - //build array - $uuids[] = "'".$record['uuid']."'"; - //assign menu item groups - $array['menu_item_groups'][$x]['menu_item_group_uuid'] = uuid(); - $array['menu_item_groups'][$x]['menu_uuid'] = $menu_uuid; - $array['menu_item_groups'][$x][$this->name.'_uuid'] = $record['uuid']; - $array['menu_item_groups'][$x]['group_name'] = $group['group_name']; - $array['menu_item_groups'][$x]['group_uuid'] = $group['group_uuid']; - //increment - $x++; - } - } - - unset($records); - - //exlude exist rows - if (!empty($array) && @sizeof($array) != 0) { - $sql = "select menu_uuid, menu_item_uuid, "; - $sql .= "group_uuid from v_menu_item_groups "; - $menu_item_groups = $this->database->select($sql, null, 'all'); - $array['menu_item_groups'] = array_filter($array['menu_item_groups'], function($ar) use ($menu_item_groups) { - foreach ($menu_item_groups as $existingArrayItem) { - if ($ar['menu_uuid'] == $existingArrayItem['menu_uuid'] && $ar['menu_item_uuid'] == $existingArrayItem['menu_item_uuid'] && $ar['group_uuid'] == $existingArrayItem['group_uuid']) { - return false; - } - } - return true; - }); - unset($menu_item_groups); - } - - //add the checked rows fro group - if (!empty($array) && is_array($array) && @sizeof($array) != 0) { - //execute save - $this->database->save($array); - unset($array); - //set message - message::add($text['message-add']); - } - } - } - } - - public function unassign_items($records, $menu_uuid, $group_uuid) { - //assign the variables - $this->name = 'menu_item'; - $this->table = 'menu_items'; - - if (permission_exists($this->name.'_add')) { - - //add multi-lingual support - $language = new text; - $text = $language->get(); - - //validate the token - $token = new token; - if (!$token->validate('/core/menu/menu_item_list.php')) { - message::add($text['message-invalid_token'],'negative'); - header('Location: '.$this->location); - exit; - } - - //assign multiple records - if (is_array($records) && @sizeof($records) != 0 && !empty($group_uuid)) { - - //define the group_name, group_uuid, menu_uuid - if (!empty($records) && @sizeof($records) != 0) { - $sql = "select group_name, group_uuid from v_groups "; - $sql .= "where group_uuid = :group_uuid "; - $parameters['group_uuid'] = $group_uuid; - $group = $this->database->select($sql, $parameters, 'row'); - } - - //build the delete array - $x = 0; - foreach ($records as $record) { - if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { - //build array - $uuids[] = "'".$record['uuid']."'"; - //assign menu item groups - $array['menu_item_groups'][$x]['menu_uuid'] = $menu_uuid; - $array['menu_item_groups'][$x][$this->name.'_uuid'] = $record['uuid']; - $array['menu_item_groups'][$x]['group_name'] = $group['group_name']; - $array['menu_item_groups'][$x]['group_uuid'] = $group['group_uuid']; - //increment - $x++; - } - } - - unset($records); - - //include child menu items and their main_uuid too - if (!empty($uuids) && @sizeof($uuids) != 0) { - $sql = "select menu_uuid, menu_item_uuid as uuid from v_".$this->table." "; - $sql .= "where menu_item_parent_uuid in (".implode(', ', $uuids).") "; - $rows = $this->database->select($sql, null, 'all'); - if (!empty($rows) && @sizeof($rows) != 0) { - foreach ($rows as $row) { - //assign menu item groups - $array['menu_item_groups'][$x]['menu_uuid'] = $row['menu_uuid']; - $array['menu_item_groups'][$x][$this->name.'_uuid'] = $row['uuid']; - $array['menu_item_groups'][$x]['group_name'] = $group['group_name']; - $array['menu_item_groups'][$x]['group_uuid'] = $group['group_uuid']; - //increment - $x++; - } - } - } - - unset($uuids); - - //add the checked rows fro group - if (!empty($array) && is_array($array) && @sizeof($array) != 0) { - //grant temporary permissions - $p = new permissions; - $p->add('menu_language_delete', 'temp'); - $p->add('menu_item_group_delete', 'temp'); - - //execute delete - $this->database->delete($array); - unset($array); - - //revoke temporary permissions - $p->delete('menu_language_delete', 'temp'); - $p->delete('menu_item_group_delete', 'temp'); - - //set message - message::add($text['message-delete']); - } - } - } - } - - /** - * restore the default menu - */ - public function restore_default() { - - //get the $apps array from the installed apps from the core and mod directories - $config_list = glob($_SERVER["DOCUMENT_ROOT"].PROJECT_PATH."/*/*/app_menu.php"); + //delete multiple records + if (is_array($records) && @sizeof($records) != 0) { + //build the delete array $x = 0; - if (is_array($config_list)) { - foreach ($config_list as $config_path) { - $app_path = dirname($config_path); - $app_path = preg_replace('/\A.*(\/.*\/.*)\z/', '$1', $app_path); - $y = 0; - try { - //echo "[".$x ."] ".$config_path."\n"; - include($config_path); + foreach ($records as $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + //build array + $uuids[] = "'" . $record['uuid'] . "'"; + //remove menu languages + $array['menu_languages'][$x][$this->name . '_uuid'] = $record['uuid']; + //remove menu item groups + $array['menu_item_groups'][$x][$this->name . '_uuid'] = $record['uuid']; + //remove menu items + $array[$this->table][$x][$this->name . '_uuid'] = $record['uuid']; + //increment + $x++; + } + } + + //include child menu items + if (!empty($uuids) && @sizeof($uuids) != 0) { + $sql = "select menu_item_uuid as uuid from v_" . $this->table . " "; + $sql .= "where menu_item_parent_uuid in (" . implode(', ', $uuids) . ") "; + $rows = $this->database->select($sql, null, 'all'); + if (!empty($rows) && @sizeof($rows) != 0) { + foreach ($rows as $row) { + //remove menu languages + $array['menu_languages'][$x][$this->name . '_uuid'] = $row['uuid']; + //remove menu item groups + $array['menu_item_groups'][$x][$this->name . '_uuid'] = $row['uuid']; + //remove menu items + $array[$this->table][$x][$this->name . '_uuid'] = $row['uuid']; + //increment $x++; } - catch (Exception $e) { - echo 'exception caught: ' . $e->getMessage() . "\n"; - exit; - } } } - //get the list of languages - $language = new text; - - //create a uuid array of the original uuid used as the key and new uuid as the value - if (is_array($apps)) { - $x = 0; - foreach ($apps as $row) { - if (is_array($row['menu'])) { - foreach ($row['menu'] as $menu) { - $uuid_array[$menu['uuid']] = uuid(); - } - } - } - } - - //if the item uuid is not currently in the db then add it - $sql = "select * from v_menu_items "; - $sql .= "where menu_uuid = :menu_uuid "; - $parameters['menu_uuid'] = $this->menu_uuid; - $menu_items = $this->database->select($sql, $parameters, 'all'); - - //use the app array to restore the default menu - if (is_array($apps)) { - $x = 0; - foreach ($apps as $row) { - if (is_array($row['menu'])) { - foreach ($row['menu'] as $menu) { - //set the variables - if (!empty($menu['title'][$this->menu_language])) { - $menu_item_title = $menu['title'][$this->menu_language]; - } - else { - $menu_item_title = $menu['title']['en-us']; - } - $uuid = $menu['uuid']; - $menu_item_uuid = $uuid_array[$menu['uuid']]; - $menu_item_parent_uuid = $uuid_array[$menu['parent_uuid']] ?? null; - $menu_item_category = $menu['category']; - $menu_item_icon = $menu['icon'] ?? null; - $menu_item_icon_color = $menu['icon_color'] ?? null; - $menu_item_path = $menu['path']; - $menu_item_order = $menu['order'] ?? null; - $menu_item_description = $menu['desc'] ?? null; - - //sanitize the menu link - $menu_item_path = preg_replace('#[^a-zA-Z0-9_:\-\.\&\=\?\/]#', '', $menu_item_path); - - //check if the menu item exists and if it does set the row array - $menu_item_exists = false; - foreach ($menu_items as $item) { - if ($item['uuid'] == $menu['uuid']) { - $menu_item_exists = true; - $row = $item; - } - } - - //item exists in the database - if ($menu_item_exists) { - $parent_menu_item_protected = 'false'; - //get parent_menu_item_protected - foreach ($menu_items as $item) { - if ($item['uuid'] == $menu['parent_uuid']) { - $parent_menu_item_protected = $item['menu_item_protected']; - } - } - - //parent is not protected so the parent uuid needs to be updated - if (is_uuid($menu_item_parent_uuid) && $menu_item_parent_uuid != $row['menu_item_parent_uuid'] && $parent_menu_item_protected != 'true') { - $array['menu_items'][$x]['menu_item_uuid'] = $row['menu_item_uuid']; - $array['menu_items'][$x]['menu_item_parent_uuid'] = $menu_item_parent_uuid; - $x++; - } - } - - //item does not exist in the database - if (!$menu_item_exists) { - if ($menu_item_uuid != $menu_item_parent_uuid) { - $array['menu_items'][$x]['menu_item_uuid'] = $menu_item_uuid; - $array['menu_items'][$x]['menu_uuid'] = $this->menu_uuid; - $array['menu_items'][$x]['uuid'] = $uuid; - $array['menu_items'][$x]['menu_item_title'] = $menu_item_title; - $array['menu_items'][$x]['menu_item_link'] = $menu_item_path; - $array['menu_items'][$x]['menu_item_category'] = $menu_item_category; - $array['menu_items'][$x]['menu_item_icon'] = $menu_item_icon; - $array['menu_items'][$x]['menu_item_icon_color'] = $menu_item_icon_color; - if (!empty($menu_item_order)) { - $array['menu_items'][$x]['menu_item_order'] = $menu_item_order; - } - if (is_uuid($menu_item_parent_uuid)) { - $array['menu_items'][$x]['menu_item_parent_uuid'] = $menu_item_parent_uuid; - } - $array['menu_items'][$x]['menu_item_description'] = $menu_item_description; - $x++; - } - } - unset($field, $parameters, $num_rows); - - //set the menu languages - if (!$menu_item_exists && is_array($language->languages)) { - foreach ($language->languages as $menu_language) { - //set the menu item title - if (!empty($menu["title"][$menu_language])) { - $menu_item_title = $menu["title"][$menu_language]; - } - else { - $menu_item_title = $menu["title"]['en-us']; - } - - //build insert array - $array['menu_languages'][$x]['menu_language_uuid'] = uuid(); - $array['menu_languages'][$x]['menu_item_uuid'] = $menu_item_uuid; - $array['menu_languages'][$x]['menu_uuid'] = $this->menu_uuid; - $array['menu_languages'][$x]['menu_language'] = $menu_language; - $array['menu_languages'][$x]['menu_item_title'] = $menu_item_title; - $x++; - } - } - } - } - } - if (is_array($array) && @sizeof($array) != 0) { - //grant temporary permissions - $p = permissions::new(); - $p->add('menu_item_add', 'temp'); - $p->add('menu_language_add', 'temp'); - //execute insert - $this->database->save($array); - unset($array); - //revoke temporary permissions - $p->delete('menu_item_add', 'temp'); - $p->delete('menu_language_add', 'temp'); - } - } - - //make sure the default user groups exist - $group = new groups; - $group->defaults(); - - //get default global group_uuids - $sql = "select group_uuid, group_name from v_groups "; - $sql .= "where domain_uuid is null "; - $result = $this->database->select($sql, null, 'all'); - if (is_array($result) && @sizeof($result) != 0) { - foreach ($result as $row) { - $group_uuids[$row['group_name']] = $row['group_uuid']; - } - } - unset($sql, $result, $row); - - //if there are no groups listed in v_menu_item_groups under menu_item_uuid then add the default groups - if (is_array($apps)) { - $x = 0; - foreach ($apps as $app) { - if (is_array($apps)) { - foreach ($app['menu'] as $sub_row) { - if (isset($sub_row['groups'])) { - foreach ($sub_row['groups'] as $group) { - $sql = "select count(*) from v_menu_item_groups "; - $sql .= "where menu_item_uuid = :menu_item_uuid "; - $sql .= "and menu_uuid = :menu_uuid "; - $sql .= "and group_name = :group_name "; - $sql .= "and group_uuid = :group_uuid "; - $parameters['menu_item_uuid'] = $uuid_array[$sub_row['uuid']]; - $parameters['menu_uuid'] = $this->menu_uuid; - $parameters['group_name'] = $group; - $parameters['group_uuid'] = $group_uuids[$group] ?? null; - $num_rows = $this->database->select($sql, $parameters, 'column'); - if ($num_rows == 0) { - //no menu item groups found, build insert array for defaults - $array['menu_item_groups'][$x]['menu_item_group_uuid'] = uuid(); - $array['menu_item_groups'][$x]['menu_uuid'] = $this->menu_uuid; - $array['menu_item_groups'][$x]['menu_item_uuid'] = $uuid_array[$sub_row['uuid']]; - $array['menu_item_groups'][$x]['group_name'] = $group; - $array['menu_item_groups'][$x]['group_uuid'] = $group_uuids[$group] ?? null; - $x++; - } - unset($sql, $parameters, $num_rows); - } - } - } - } - } - - if (is_array($array) && @sizeof($array) != 0) { - //grant temporary permissions - $p = permissions::new(); - $p->add('menu_item_group_add', 'temp'); - //execute insert - $this->database->save($array); - unset($array); - //revoke temporary permissions - $p->delete('menu_item_group_add', 'temp'); - } - } - - } - - /** - * create the menu - */ - public function build_html($menu_item_level = 0) { - - $menu_html_full = ''; - - $menu_array = $this->menu_array(); - - if (!isset($_SESSION['groups'])) { - $_SESSION['groups'][0]['group_name'] = 'public'; - } - - if (is_array($menu_array)) { - foreach($menu_array as $menu_field) { - //set the variables - $menu_item_link = $menu_field['menu_item_link']; - $menu_item_category = $menu_field['menu_item_category']; - $menu_items = $menu_field['menu_items']; - - //prepare the protected menus - //$menu_item_title = ($menu_field['menu_item_protected'] == "true") ? $menu_field['menu_item_title'] : $menu_field['menu_language_title']; - $menu_item_title = $menu_field['menu_language_title']; - - //prepare the menu_tags according to the category - $menu_tags = ''; - switch ($menu_item_category) { - case "internal": - $menu_tags = "href='".PROJECT_PATH.$menu_item_link."'"; - break; - case "external": - if (substr($menu_item_link, 0,1) == "/") { - $menu_item_link = PROJECT_PATH.$menu_item_link; - } - $menu_tags = "href='".$menu_item_link."' target='_blank'"; - break; - case "email": - $menu_tags = "href='mailto:".$menu_item_link."'"; - break; - } - - if ($menu_item_level == 0) { - $menu_html = "\n\n"; - } - - $menu_html_full .= $menu_html; - } //end for each - } - - return $menu_html_full; - } - - /** - * create the sub menus - */ - private function build_child_html($menu_item_level, $submenu_array) { - - $menu_item_level = $menu_item_level+1; - - if (count($_SESSION['groups']) == 0) { - $_SESSION['groups'][0]['group_name'] = 'public'; - } - - if (is_array($submenu_array)) { - //child menu found - $submenu_html = "\n"; - - return $submenu_html; - } - } - - /** - * create the menu array - */ - public function menu_array($menu_item_level = 0) { - - //if there are no groups then set the public group - if (!isset($_SESSION['groups'][0]['group_name'])) { - $_SESSION['groups'][0]['group_name'] = 'public'; - } - - //get the menu from the database - $sql = "select i.menu_item_link, l.menu_item_title as menu_language_title, "; - $sql .= "i.menu_item_title, i.menu_item_category, i.menu_item_icon, "; - $sql .= "i.menu_item_icon_color, i.menu_item_uuid, i.menu_item_parent_uuid "; - $sql .= "from v_menu_items as i, v_menu_languages as l "; - $sql .= "where i.menu_item_uuid = l.menu_item_uuid "; - $sql .= "and l.menu_language = :menu_language "; - $sql .= "and l.menu_uuid = :menu_uuid "; - $sql .= "and i.menu_uuid = :menu_uuid "; - $sql .= "and i.menu_item_parent_uuid is null "; - $sql .= "and i.menu_item_uuid in "; - $sql .= "( "; - $sql .= "select menu_item_uuid "; - $sql .= "from v_menu_item_groups "; - $sql .= "where menu_uuid = :menu_uuid "; - $x = 0; - foreach($_SESSION['groups'] as $row) { - $sql_where_or[] = "group_name = :group_name_".$x; - $parameters['group_name_'.$x] = $row['group_name']; - $x++; - } - if (is_array($sql_where_or) && @sizeof($sql_where_or) != 0) { - $sql .= "and ( "; - $sql .= implode(' or ', $sql_where_or); - $sql .= ") "; - } - $sql .= "and menu_item_uuid is not null "; - $sql .= ") "; - $sql .= "order by i.menu_item_order asc "; - $parameters['menu_language'] = $this->settings->get('domain', 'language', 'en-us'); - $parameters['menu_uuid'] = $this->menu_uuid; - $result = $this->database->select($sql, $parameters, 'all'); - unset($sql, $parameters); - - //save the menu into an array - $x = 0; - $a = Array(); - if (is_array($result) && @sizeof($result) != 0) { - foreach($result as $row) { - //add the row to the array - $a[$x] = $row; - - //add the sub menus to the array - $menu_item_level = 0; - if (!empty($row['menu_item_uuid'])) { - $a[$x]['menu_items'] = $this->menu_child_array($menu_item_level, $row['menu_item_uuid']); - } - - //increment the row number - $x++; - } - } - unset($result, $row); - - //return the array - return $a; - } - - /** - * create the sub menus - */ - private function menu_child_array($menu_item_level, $menu_item_uuid) { - - //set the level - $menu_item_level++; - - //if there are no groups then set the public group - if (!isset($_SESSION['groups'][0]['group_name'])) { - $_SESSION['groups'][0]['group_name'] = 'public'; - } - - //get the child menu from the database - $sql = "select i.menu_item_link, l.menu_item_title as menu_language_title, "; - $sql .= "i.menu_item_title, i.menu_item_category, i.menu_item_icon, "; - $sql .= "i.menu_item_icon_color, i.menu_item_uuid, i.menu_item_parent_uuid "; - $sql .= "from v_menu_items as i, v_menu_languages as l "; - $sql .= "where i.menu_item_uuid = l.menu_item_uuid "; - $sql .= "and l.menu_language = :menu_language "; - $sql .= "and l.menu_uuid = :menu_uuid "; - $sql .= "and i.menu_uuid = :menu_uuid "; - $sql .= "and i.menu_item_parent_uuid = :menu_item_parent_uuid "; - $sql .= "and i.menu_item_uuid in "; - $sql .= "( "; - $sql .= "select menu_item_uuid "; - $sql .= "from v_menu_item_groups "; - $sql .= "where menu_uuid = :menu_uuid "; - $x = 0; - foreach($_SESSION['groups'] as $row) { - $sql_where_or[] = "group_name = :group_name_".$x; - $parameters['group_name_'.$x] = $row['group_name']; - $x++; - } - if (is_array($sql_where_or) && @sizeof($sql_where_or) != 0) { - $sql .= "and ( "; - $sql .= implode(' or ', $sql_where_or); - $sql .= ") "; - } - $sql .= ") "; - $sql .= "order by l.menu_item_title, i.menu_item_order asc "; - $parameters['menu_language'] = $this->settings->get('domain', 'language', 'en-us'); - $parameters['menu_uuid'] = $this->menu_uuid; - $parameters['menu_item_parent_uuid'] = $menu_item_uuid; - $sub_result = $this->database->select($sql, $parameters, 'all'); - unset($sql, $parameters); - - //save the child menu into an array - $x = 0; - $a = Array(); - if (is_array($sub_result) && @sizeof($sub_result) != 0) { - foreach($sub_result as $row) { - //set the variables - $menu_item_link = $row['menu_item_link']; - $menu_item_category = $row['menu_item_category']; - $menu_item_icon = $row['menu_item_icon']; - $menu_item_icon_color = $row['menu_item_icon_color']; - $menu_item_uuid = $row['menu_item_uuid']; - $menu_item_parent_uuid = $row['menu_item_parent_uuid']; - - //add the row to the array - $a[$x] = $row; - - //prepare the menus - //if ($row['menu_item_protected'] == "true") { - // $a[$x]['menu_item_title'] = $row['menu_item_title']; - //} - //else { - $a[$x]['menu_item_title'] = $row['menu_language_title']; - //} - - //get sub menu for children - if (!empty($menu_item_uuid)) { - $a[$x]['menu_items'] = $this->menu_child_array($menu_item_level, $menu_item_uuid); - } - - //increment the row - $x++; - } - } - unset($sub_result, $row); - - //return the array - return $a; - } - - /** - * add the default menu when no menu exists - */ - public function menu_default() { - //set the default menu_uuid - $this->menu_uuid = 'b4750c3f-2a86-b00d-b7d0-345c14eca286'; - //check to see if any menu exists - $sql = "select count(*) as count from v_menus "; - $sql .= "where menu_uuid = :menu_uuid "; - $parameters['menu_uuid'] = $this->menu_uuid; - $num_rows = $this->database->select($sql, $parameters, 'column'); - if ($num_rows == 0) { - //built insert array - $array['menus'][0]['menu_uuid'] = $this->menu_uuid; - $array['menus'][0]['menu_name'] = 'default'; - $array['menus'][0]['menu_language'] = 'en-us'; - $array['menus'][0]['menu_description'] = 'Default Menu'; + //delete the checked rows + if (!empty($array) && is_array($array) && @sizeof($array) != 0) { //grant temporary permissions - $p = permissions::new(); - $p->add('menu_add', 'temp'); + $p = permissions::new(); + $p->add('menu_language_delete', 'temp'); + $p->add('menu_item_group_delete', 'temp'); - //execute insert - $this->database->save($array); - unset($array); + //execute delete + $this->database->delete($array); + unset($array); //revoke temporary permissions - $p->delete('menu_add', 'temp'); + $p->delete('menu_language_delete', 'temp'); + $p->delete('menu_item_group_delete', 'temp'); - //add the menu items - $this->restore_default(); + //set message + message::add($this->text['message-delete']); } - } - - /** - * build the fixed, static or inline horizontal menu html - * @param array $menu_array Associative array of menu items - */ - public function menu_horizontal($menu_array) { - - //determine menu behavior - $menu_style = $this->settings->get('theme', 'menu_style', 'fixed'); - switch ($menu_style) { - case 'inline': - $menu_type = 'default'; - $menu_width = 'calc(100% - 20px)'; - $menu_brand = false; - $menu_corners = null; - break; - case 'static': - $menu_type = 'static-top'; - $menu_width = 'calc(100% - 40px)'; - $menu_brand = true; - $menu_corners = "style='-webkit-border-radius: 0 0 4px 4px; -moz-border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;'"; - break; - case 'fixed': - default: - $menu_type = 'fixed-'.$this->settings->get('theme', 'menu_position', 'top'); - if (!http_user_agent('mobile')) { - $menu_width = $this->settings->get('theme', 'menu_width_fixed', 'calc(90% - 20px)'); - } - $menu_brand = true; - $menu_corners = null; - } - - //begin navbar code - $html = "\n"; - - //user menu on menu bar - //styles below are defined here to prevent caching (following a permission change, etc) - $html .= "\n"; - - $html .= "
    \n"; - $html .= "
    \n"; - if (!empty($_SESSION['user']['contact_image']) && is_uuid($_SESSION['user']['contact_image'])) { - $html .= "
    \n"; - } - else { - $html .= "
    text['label-primary-contact-attachment-image']."\">
    \n"; - } - // $html .= "
    \n"; - $html .= "
    \n"; - if (!empty($_SESSION['user']['contact_name'])) { - $html .= "
    ".$_SESSION['user']['contact_name']."
    \n"; - } - if (!empty($_SESSION['user']['contact_organization'])) { - $html .= "
    ".$_SESSION['user']['contact_organization']."
    \n"; - } - if (!empty($_SESSION['user']['extension'][0]['destination'])) { - $html .= "
    ".$_SESSION['user']['extension'][0]['destination']."
    \n"; - } - $html .= "
    \n"; - $html .= " ".$this->text['title-user_profile']."
    \n"; - $html .= " ".$this->text['title-logout']."\n"; - $html .= "
    "; - $html .= "
    "; - $html .= "
    "; - $html .= "
    "; - - //modal for logout icon (above) - if (!empty($this->username) && $this->settings->get('theme', 'logout_icon_visible', 'false') == "true") { - $html .= modal::create(['id'=>'modal-logout','type'=>'general','message'=>$this->text['theme-confirm-logout'],'actions'=>button::create(['type'=>'button','label'=>$this->text['theme-label-logout'],'icon'=>'fa-solid fa-right-from-bracket','id'=>'btn_logout','style'=>'float: right; margin-left: 15px;','collapse'=>'never','link'=>PROJECT_PATH.'/logout.php','onclick'=>"modal_close();"])]); - } - - return $html; - } - - /** - * build the vertical side menu html - * @param array $menu_array Associative array of menu items - */ - public function menu_vertical($menu_array) { - //set defaults - $menu_side_state = $this->settings->get('theme', 'menu_side_state', 'contracted'); - $menu_side_state_class = $menu_side_state !== 'hidden' ? 'hide-sm-up ' : ''; - //menu brand image and/or text - $html = " \n"; - //main menu items - if (!empty($menu_array)) { - foreach ($menu_array as $menu_item_main) { - $menu_target = ($menu_item_main['menu_item_category'] == 'external') ? '_blank' : ''; - $html .= " "; - if (is_array($menu_item_main['menu_items']) && sizeof($menu_item_main['menu_items']) != 0 && $this->settings->get('theme', 'menu_side_item_main_sub_icons', true) === true) { - $html .= " \n"; - } - if (!empty($menu_item_main['menu_item_icon']) && substr($menu_item_main['menu_item_icon'], 0, 3) == 'fa-') { // font awesome icon - $html .= ""; - } - $html .= "".$menu_item_main['menu_language_title'].""; - $html .= "\n"; - //sub menu items - if (is_array($menu_item_main['menu_items']) && sizeof($menu_item_main['menu_items']) != 0) { - $html .= " \n"; - } - } - $html .= "
    \n"; - } - $html .= "
    \n"; - $content_container_onclick = ""; - if ($menu_side_state != 'expanded') { - $content_container_onclick = "onclick=\"clearTimeout(menu_side_contract_timer); if ($(window).width() >= 576) { menu_side_contract(); }\""; + unset($records); } - $html .= "
    \n"; + } + } - //user menu on body header when side menu - //styles below are defined here to prevent caching (following a permission change, etc) - $html .= "\n"; + /** + * Deletes one or multiple records. + * + * @param array $records An array of record IDs to delete, where each ID is an associative array + * containing 'uuid' and 'checked' keys. The 'checked' value indicates + * whether the corresponding checkbox was checked for deletion. + * + * @return void No return value; this method modifies the database state and sets a message. + */ + public function delete($records) { + //assign the variables + $this->name = 'menu'; + $this->table = 'menus'; - $html .= "
    \n"; - $html .= "
    \n"; - if (!empty($_SESSION['user']['contact_image']) && is_uuid($_SESSION['user']['contact_image'])) { - $html .= "
    \n"; - } - else { - $html .= "
    text['label-primary-contact-attachment-image']."\">
    \n"; - } - // $html .= "
    \n"; - $html .= "
    \n"; - if (!empty($_SESSION['user']['contact_name'])) { - $html .= "
    ".$_SESSION['user']['contact_name']."
    \n"; - } - if (!empty($_SESSION['user']['contact_organization'])) { - $html .= "
    ".$_SESSION['user']['contact_organization']."
    \n"; - } - if (!empty($_SESSION['user']['extension'][0]['destination'])) { - $html .= "
    ".$_SESSION['user']['extension'][0]['destination']."
    \n"; - } - $html .= "
    \n"; - $html .= " ".$this->text['title-user_profile']."
    \n"; - $html .= " ".$this->text['title-logout']."\n"; - $html .= "
    "; - $html .= "
    "; - $html .= "
    "; - $html .= "
    "; + if (permission_exists($this->name . '_delete')) { - $html .= "
    \n"; - //header: left - $html .= "
    \n"; - // $html .= button::create(['type'=>'button','id'=>'menu_side_state_hidden_button','title'=>$this->text['theme-label-expand_menu'],'icon'=>'bars','class'=>'default '.($this->settings->get('theme', 'menu_side_state') != 'hidden' ? 'hide-sm-up ' : null).'float-left','onclick'=>'menu_side_expand();']); - $html .= "text['theme-label-expand_menu']."\">"; - $body_header_brand_text = escape($this->settings->get('theme', 'body_header_brand_text', 'FusionPBX')); - if ($this->settings->get('theme', 'body_header_brand_type') == 'image' || $this->settings->get('theme', 'body_header_brand_type') == 'image_text') { - $body_header_brand_image = $this->settings->get('theme', 'body_header_brand_image', PROJECT_PATH.'/themes/default/images/logo_side_expanded.png'); - $html .= "
    "; - $html .= ""; - $html .= "
    "; - } - if ($this->settings->get('theme', 'body_header_brand_type') == 'text' || $this->settings->get('theme', 'body_header_brand_type') == 'image_text') { - $html .= ""; - } - $html .= "
    \n"; - //header: right - $html .= "
    "; - //current user - //set (default) user graphic size and icon - $user_graphic_size = 18; - $user_graphic = ""; - //overwrite user graphic with image from session, if exists - if ($this->settings->get('theme', 'body_header_user_image', true) === true && !empty($_SESSION['user']['contact_image']) && is_uuid($_SESSION['user']['contact_image'])) { - $user_graphic_size = str_replace(['px','%'], '', intval($this->settings->get('theme', 'body_header_user_image_size', 18))); - $user_graphic = " 18 ? '-'.(ceil(($user_graphic_size - 18) / 2) - 4) : '-4')."px; background-image: url('".PROJECT_PATH."/core/contacts/contact_attachment.php?id=".$_SESSION['user']['contact_image']."&action=download&sid=".session_id()."'); background-repeat: no-repeat; background-size: cover; background-position: center;\">"; + //validate the token + $token = new token; + if (!$token->validate($_SERVER['PHP_SELF'])) { + message::add($this->text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; + } + + //delete multiple records + if (is_array($records) && @sizeof($records) != 0) { + //build the delete array + $x = 0; + foreach ($records as $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + //remove menu languages + $array['menu_languages'][$x][$this->name . '_uuid'] = $record['uuid']; + + //remove menu item groups + $array['menu_item_groups'][$x][$this->name . '_uuid'] = $record['uuid']; + + //remove menu items + $array['menu_items'][$x][$this->name . '_uuid'] = $record['uuid']; + + //build array to remove the menu + $array[$this->table][$x][$this->name . '_uuid'] = $record['uuid']; + + //increment + $x++; } - $html .= "\n"; - $html .= " username."\" onclick=\"event.preventDefault(); $('#body_header_user_menu').toggleFadeSlide();\">".($user_graphic ?? null)."".escape($this->username).""; - $html .= "\n"; - //domain name/selector (sm+) - if (!empty($this->username) && permission_exists('domain_select') && count($_SESSION['domains']) > 1 && $this->settings->get('theme', 'domain_visible') == 'true') { - $html .= "\n"; - $html .= " ".escape($this->domain_name).""; - $html .= "\n"; - } - //logout icon - if (!empty($this->username) && $this->settings->get('theme', 'logout_icon_visible') == "true") { - $html .= "text['theme-label-logout']."\" onclick=\"modal_open('modal-logout','btn_logout');\">"; - } - $html .= "
    "; - $html .= "
    \n"; - - //modal for logout icon (above) - if (!empty($this->username) && $this->settings->get('theme', 'logout_icon_visible') == "true") { - $html .= modal::create(['id'=>'modal-logout','type'=>'general','message'=>$this->text['theme-confirm-logout'],'actions'=>button::create(['type'=>'button','label'=>$this->text['theme-label-logout'],'icon'=>'fa-solid fa-right-from-bracket','id'=>'btn_logout','style'=>'float: right; margin-left: 15px;','collapse'=>'never','link'=>PROJECT_PATH.'/logout.php','onclick'=>"modal_close();"])]); } - return $html; + //delete the checked rows + if (is_array($array) && @sizeof($array) != 0) { + //grant temporary permissions + $p = permissions::new(); + $p->add('menu_item_delete', 'temp'); + $p->add('menu_item_group_delete', 'temp'); + $p->add('menu_language_delete', 'temp'); + + //execute delete + $this->database->delete($array); + unset($array); + + //revoke temporary permissions + $p->delete('menu_item_delete', 'temp'); + $p->delete('menu_item_group_delete', 'temp'); + $p->delete('menu_language_delete', 'temp'); + + //set message + message::add($this->text['message-delete']); + } + unset($records); + } + } + } + + /** + * Toggles the state of one or more records. + * + * @param array $records An array of record IDs to delete, where each ID is an associative array + * containing 'uuid' and 'checked' keys. The 'checked' value indicates + * whether the corresponding checkbox was checked for deletion. + * + * @return void No return value; this method modifies the database state and sets a message. + */ + public function toggle_items($records) { + //assign the variables + $this->name = 'menu_item'; + $this->table = 'menu_items'; + $this->toggle_field = 'menu_item_protected'; + $this->toggle_values = ['true', 'false']; + + if (permission_exists($this->name . '_edit')) { + + //validate the token + $token = new token; + if (!$token->validate('/core/menu/menu_item_list.php')) { + message::add($this->text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; + } + + //toggle the checked records + if (is_array($records) && @sizeof($records) != 0) { + //get current toggle state + foreach ($records as $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + $uuids[] = "'" . $record['uuid'] . "'"; + } + } + if (!empty($uuids) && is_array($uuids) && @sizeof($uuids) != 0) { + $sql = "select " . $this->name . "_uuid as uuid, " . $this->toggle_field . " as toggle from v_" . $this->table . " "; + $sql .= "where " . $this->name . "_uuid in (" . implode(', ', $uuids) . ") "; + $parameters = null; + $rows = $this->database->select($sql, $parameters, 'all'); + if (is_array($rows) && @sizeof($rows) != 0) { + foreach ($rows as $row) { + $states[$row['uuid']] = $row['toggle'] == '' ? $this->toggle_values[1] : $row['toggle']; + } + } + unset($sql, $parameters, $rows, $row); + } + + //build update array + $x = 0; + if (!empty($states) && is_array($states) && @sizeof($states) != 0) { + foreach ($states as $uuid => $state) { + //create the array + $array[$this->table][$x][$this->name . '_uuid'] = $uuid; + $array[$this->table][$x][$this->toggle_field] = $state == $this->toggle_values[0] ? $this->toggle_values[1] : $this->toggle_values[0]; + + //increment + $x++; + } + } + + //save the changes + if (!empty($array) && is_array($array) && @sizeof($array) != 0) { + //save the array + $this->database->save($array); + unset($array); + + //set message + message::add($this->text['message-toggle']); + } + unset($records, $states); + } + } + } + + /** + * Restore the deleted items from a menu + */ + public function restore_delete() { + //remove existing menu languages + $sql = "delete from v_menu_languages "; + $sql .= "where menu_uuid = :menu_uuid "; + $sql .= "and menu_item_uuid in ( "; + $sql .= " select menu_item_uuid "; + $sql .= " from v_menu_items "; + $sql .= " where menu_uuid = :menu_uuid "; + //$sql .= " and ( "; + //$sql .= " menu_item_protected <> 'true' "; + //$sql .= " or menu_item_protected is null "; + //$sql .= " ) "; + $sql .= ") "; + $parameters['menu_uuid'] = $this->menu_uuid; + $this->database->execute($sql, $parameters); + unset($sql, $parameters); + + //remove existing menu item groups + $sql = "delete from v_menu_item_groups "; + $sql .= "where menu_uuid = :menu_uuid "; + $sql .= "and menu_item_uuid in ( "; + $sql .= " select menu_item_uuid "; + $sql .= " from v_menu_items "; + $sql .= " where menu_uuid = :menu_uuid "; + //$sql .= " and ( "; + //$sql .= " menu_item_protected <> 'true' "; + //$sql .= " or menu_item_protected is null "; + //$sql .= " ) "; + $sql .= ") "; + $parameters['menu_uuid'] = $this->menu_uuid; + $this->database->execute($sql, $parameters); + unset($sql, $parameters); + + //remove existing menu items + $sql = "delete from v_menu_items "; + $sql .= "where menu_uuid = :menu_uuid "; + //$sql .= "and ( "; + //$sql .= " menu_item_protected <> 'true' "; + //$sql .= " or menu_item_protected is null "; + //$sql .= ") "; + $parameters['menu_uuid'] = $this->menu_uuid; + $this->database->execute($sql, $parameters); + unset($sql, $parameters); + } + + /** + * Assigns multiple menu items to a group. + * + * @param array $records Array of records containing the IDs and UUIDs of the items to be assigned. + * @param string $menu_uuid The unique identifier for the menu. + * @param string $group_uuid The unique identifier for the group. + */ + public function assign_items($records, $menu_uuid, $group_uuid) { + //assign the variables + $this->name = 'menu_item'; + $this->table = 'menu_items'; + + if (permission_exists($this->name . '_add')) { + + //add multi-lingual support + $language = new text; + $text = $language->get(); + + //validate the token + $token = new token; + if (!$token->validate('/core/menu/menu_item_list.php')) { + message::add($text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; + } + + //assign multiple records + if (is_array($records) && @sizeof($records) != 0 && !empty($group_uuid)) { + + //define the group_name, group_uuid, menu_uuid + if (!empty($records) && @sizeof($records) != 0) { + $sql = "select group_name, group_uuid from v_groups "; + $sql .= "where group_uuid = :group_uuid "; + $parameters['group_uuid'] = $group_uuid; + $group = $this->database->select($sql, $parameters, 'row'); + } + + //build the delete array + $x = 0; + foreach ($records as $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + //build array + $uuids[] = "'" . $record['uuid'] . "'"; + //assign menu item groups + $array['menu_item_groups'][$x]['menu_item_group_uuid'] = uuid(); + $array['menu_item_groups'][$x]['menu_uuid'] = $menu_uuid; + $array['menu_item_groups'][$x][$this->name . '_uuid'] = $record['uuid']; + $array['menu_item_groups'][$x]['group_name'] = $group['group_name']; + $array['menu_item_groups'][$x]['group_uuid'] = $group['group_uuid']; + //increment + $x++; + } + } + + unset($records); + + //exlude exist rows + if (!empty($array) && @sizeof($array) != 0) { + $sql = "select menu_uuid, menu_item_uuid, "; + $sql .= "group_uuid from v_menu_item_groups "; + $menu_item_groups = $this->database->select($sql, null, 'all'); + $array['menu_item_groups'] = array_filter($array['menu_item_groups'], function ($ar) use ($menu_item_groups) { + foreach ($menu_item_groups as $existingArrayItem) { + if ($ar['menu_uuid'] == $existingArrayItem['menu_uuid'] && $ar['menu_item_uuid'] == $existingArrayItem['menu_item_uuid'] && $ar['group_uuid'] == $existingArrayItem['group_uuid']) { + return false; + } + } + return true; + }); + unset($menu_item_groups); + } + + //add the checked rows fro group + if (!empty($array) && is_array($array) && @sizeof($array) != 0) { + //execute save + $this->database->save($array); + unset($array); + //set message + message::add($text['message-add']); + } + } + } + } + + /** + * Unassign items from a menu group. + * + * @param array $records The list of items to unassign, where each item is an associative array containing + * 'uuid' and 'checked' keys. + * @param string $menu_uuid The UUID of the menu to unassign items from. + * @param string $group_uuid The UUID of the group that owns the menu. + * + * @return void + */ + public function unassign_items($records, $menu_uuid, $group_uuid) { + //assign the variables + $this->name = 'menu_item'; + $this->table = 'menu_items'; + + if (permission_exists($this->name . '_add')) { + + //add multi-lingual support + $language = new text; + $text = $language->get(); + + //validate the token + $token = new token; + if (!$token->validate('/core/menu/menu_item_list.php')) { + message::add($text['message-invalid_token'], 'negative'); + header('Location: ' . $this->location); + exit; + } + + //assign multiple records + if (is_array($records) && @sizeof($records) != 0 && !empty($group_uuid)) { + + //define the group_name, group_uuid, menu_uuid + if (!empty($records) && @sizeof($records) != 0) { + $sql = "select group_name, group_uuid from v_groups "; + $sql .= "where group_uuid = :group_uuid "; + $parameters['group_uuid'] = $group_uuid; + $group = $this->database->select($sql, $parameters, 'row'); + } + + //build the delete array + $x = 0; + foreach ($records as $record) { + if (!empty($record['checked']) && $record['checked'] == 'true' && is_uuid($record['uuid'])) { + //build array + $uuids[] = "'" . $record['uuid'] . "'"; + //assign menu item groups + $array['menu_item_groups'][$x]['menu_uuid'] = $menu_uuid; + $array['menu_item_groups'][$x][$this->name . '_uuid'] = $record['uuid']; + $array['menu_item_groups'][$x]['group_name'] = $group['group_name']; + $array['menu_item_groups'][$x]['group_uuid'] = $group['group_uuid']; + //increment + $x++; + } + } + + unset($records); + + //include child menu items and their main_uuid too + if (!empty($uuids) && @sizeof($uuids) != 0) { + $sql = "select menu_uuid, menu_item_uuid as uuid from v_" . $this->table . " "; + $sql .= "where menu_item_parent_uuid in (" . implode(', ', $uuids) . ") "; + $rows = $this->database->select($sql, null, 'all'); + if (!empty($rows) && @sizeof($rows) != 0) { + foreach ($rows as $row) { + //assign menu item groups + $array['menu_item_groups'][$x]['menu_uuid'] = $row['menu_uuid']; + $array['menu_item_groups'][$x][$this->name . '_uuid'] = $row['uuid']; + $array['menu_item_groups'][$x]['group_name'] = $group['group_name']; + $array['menu_item_groups'][$x]['group_uuid'] = $group['group_uuid']; + //increment + $x++; + } + } + } + + unset($uuids); + + //add the checked rows fro group + if (!empty($array) && is_array($array) && @sizeof($array) != 0) { + //grant temporary permissions + $p = new permissions; + $p->add('menu_language_delete', 'temp'); + $p->add('menu_item_group_delete', 'temp'); + + //execute delete + $this->database->delete($array); + unset($array); + + //revoke temporary permissions + $p->delete('menu_language_delete', 'temp'); + $p->delete('menu_item_group_delete', 'temp'); + + //set message + message::add($text['message-delete']); + } + } + } + } + + /** + * Builds HTML for the menu. + * + * @param int $menu_item_level The level of the current menu item (0 for top-level, 1 for child items, etc.) + * + * @return string The HTML code for the menu + */ + public function build_html($menu_item_level = 0) { + + $menu_html_full = ''; + + $menu_array = $this->menu_array(); + + if (!isset($_SESSION['groups'])) { + $_SESSION['groups'][0]['group_name'] = 'public'; + } + + if (is_array($menu_array)) { + foreach ($menu_array as $menu_field) { + //set the variables + $menu_item_link = $menu_field['menu_item_link']; + $menu_item_category = $menu_field['menu_item_category']; + $menu_items = $menu_field['menu_items']; + + //prepare the protected menus + //$menu_item_title = ($menu_field['menu_item_protected'] == "true") ? $menu_field['menu_item_title'] : $menu_field['menu_language_title']; + $menu_item_title = $menu_field['menu_language_title']; + + //prepare the menu_tags according to the category + $menu_tags = ''; + switch ($menu_item_category) { + case "internal": + $menu_tags = "href='" . PROJECT_PATH . $menu_item_link . "'"; + break; + case "external": + if (substr($menu_item_link, 0, 1) == "/") { + $menu_item_link = PROJECT_PATH . $menu_item_link; + } + $menu_tags = "href='" . $menu_item_link . "' target='_blank'"; + break; + case "email": + $menu_tags = "href='mailto:" . $menu_item_link . "'"; + break; + } + + if ($menu_item_level == 0) { + $menu_html = "\n\n"; + } + + $menu_html_full .= $menu_html; + } //end for each + } + + return $menu_html_full; + } + + /** + * Retrieves an array of menu items. + * + * @param int $menu_item_level The level of the menu items (default: 0). + * + * @return array An array of menu items and their sub menus. + */ + public function menu_array($menu_item_level = 0) { + + //if there are no groups then set the public group + if (!isset($_SESSION['groups'][0]['group_name'])) { + $_SESSION['groups'][0]['group_name'] = 'public'; + } + + //get the menu from the database + $sql = "select i.menu_item_link, l.menu_item_title as menu_language_title, "; + $sql .= "i.menu_item_title, i.menu_item_category, i.menu_item_icon, "; + $sql .= "i.menu_item_icon_color, i.menu_item_uuid, i.menu_item_parent_uuid "; + $sql .= "from v_menu_items as i, v_menu_languages as l "; + $sql .= "where i.menu_item_uuid = l.menu_item_uuid "; + $sql .= "and l.menu_language = :menu_language "; + $sql .= "and l.menu_uuid = :menu_uuid "; + $sql .= "and i.menu_uuid = :menu_uuid "; + $sql .= "and i.menu_item_parent_uuid is null "; + $sql .= "and i.menu_item_uuid in "; + $sql .= "( "; + $sql .= "select menu_item_uuid "; + $sql .= "from v_menu_item_groups "; + $sql .= "where menu_uuid = :menu_uuid "; + $x = 0; + foreach ($_SESSION['groups'] as $row) { + $sql_where_or[] = "group_name = :group_name_" . $x; + $parameters['group_name_' . $x] = $row['group_name']; + $x++; + } + if (is_array($sql_where_or) && @sizeof($sql_where_or) != 0) { + $sql .= "and ( "; + $sql .= implode(' or ', $sql_where_or); + $sql .= ") "; + } + $sql .= "and menu_item_uuid is not null "; + $sql .= ") "; + $sql .= "order by i.menu_item_order asc "; + $parameters['menu_language'] = $this->settings->get('domain', 'language', 'en-us'); + $parameters['menu_uuid'] = $this->menu_uuid; + $result = $this->database->select($sql, $parameters, 'all'); + unset($sql, $parameters); + + //save the menu into an array + $x = 0; + $a = []; + if (is_array($result) && @sizeof($result) != 0) { + foreach ($result as $row) { + //add the row to the array + $a[$x] = $row; + + //add the sub menus to the array + $menu_item_level = 0; + if (!empty($row['menu_item_uuid'])) { + $a[$x]['menu_items'] = $this->menu_child_array($menu_item_level, $row['menu_item_uuid']); + } + + //increment the row number + $x++; + } + } + unset($result, $row); + + //return the array + return $a; + } + + /** + * Retrieves a child menu array based on the provided menu item level and UUID. + * + * @param int $menu_item_level The level of the menu item. + * @param string $menu_item_uuid The UUID of the menu item. + * + * @return array An array containing the child menu items. + */ + private function menu_child_array($menu_item_level, $menu_item_uuid) { + + //set the level + $menu_item_level++; + + //if there are no groups then set the public group + if (!isset($_SESSION['groups'][0]['group_name'])) { + $_SESSION['groups'][0]['group_name'] = 'public'; + } + + //get the child menu from the database + $sql = "select i.menu_item_link, l.menu_item_title as menu_language_title, "; + $sql .= "i.menu_item_title, i.menu_item_category, i.menu_item_icon, "; + $sql .= "i.menu_item_icon_color, i.menu_item_uuid, i.menu_item_parent_uuid "; + $sql .= "from v_menu_items as i, v_menu_languages as l "; + $sql .= "where i.menu_item_uuid = l.menu_item_uuid "; + $sql .= "and l.menu_language = :menu_language "; + $sql .= "and l.menu_uuid = :menu_uuid "; + $sql .= "and i.menu_uuid = :menu_uuid "; + $sql .= "and i.menu_item_parent_uuid = :menu_item_parent_uuid "; + $sql .= "and i.menu_item_uuid in "; + $sql .= "( "; + $sql .= "select menu_item_uuid "; + $sql .= "from v_menu_item_groups "; + $sql .= "where menu_uuid = :menu_uuid "; + $x = 0; + foreach ($_SESSION['groups'] as $row) { + $sql_where_or[] = "group_name = :group_name_" . $x; + $parameters['group_name_' . $x] = $row['group_name']; + $x++; + } + if (is_array($sql_where_or) && @sizeof($sql_where_or) != 0) { + $sql .= "and ( "; + $sql .= implode(' or ', $sql_where_or); + $sql .= ") "; + } + $sql .= ") "; + $sql .= "order by l.menu_item_title, i.menu_item_order asc "; + $parameters['menu_language'] = $this->settings->get('domain', 'language', 'en-us'); + $parameters['menu_uuid'] = $this->menu_uuid; + $parameters['menu_item_parent_uuid'] = $menu_item_uuid; + $sub_result = $this->database->select($sql, $parameters, 'all'); + unset($sql, $parameters); + + //save the child menu into an array + $x = 0; + $a = []; + if (is_array($sub_result) && @sizeof($sub_result) != 0) { + foreach ($sub_result as $row) { + //set the variables + $menu_item_link = $row['menu_item_link']; + $menu_item_category = $row['menu_item_category']; + $menu_item_icon = $row['menu_item_icon']; + $menu_item_icon_color = $row['menu_item_icon_color']; + $menu_item_uuid = $row['menu_item_uuid']; + $menu_item_parent_uuid = $row['menu_item_parent_uuid']; + + //add the row to the array + $a[$x] = $row; + + //prepare the menus + //if ($row['menu_item_protected'] == "true") { + // $a[$x]['menu_item_title'] = $row['menu_item_title']; + //} + //else { + $a[$x]['menu_item_title'] = $row['menu_language_title']; + //} + + //get sub menu for children + if (!empty($menu_item_uuid)) { + $a[$x]['menu_items'] = $this->menu_child_array($menu_item_level, $menu_item_uuid); + } + + //increment the row + $x++; + } + } + unset($sub_result, $row); + + //return the array + return $a; + } + + /** + * Build the HTML for child menu items. + * + * @param int $menu_item_level The current level of menu item. + * @param array $submenu_array An array of submenu fields. + * + * @return string The HTML for child menu items. + */ + private function build_child_html($menu_item_level, $submenu_array) { + + $menu_item_level = $menu_item_level + 1; + + if (count($_SESSION['groups']) == 0) { + $_SESSION['groups'][0]['group_name'] = 'public'; + } + + if (is_array($submenu_array)) { + //child menu found + $submenu_html = "\n"; + + return $submenu_html; + } + } + + /** + * Sets and validates the default menu UUID, creating a new default menu if none exists. + * + * @return void + */ + public function menu_default() { + //set the default menu_uuid + $this->menu_uuid = 'b4750c3f-2a86-b00d-b7d0-345c14eca286'; + //check to see if any menu exists + $sql = "select count(*) as count from v_menus "; + $sql .= "where menu_uuid = :menu_uuid "; + $parameters['menu_uuid'] = $this->menu_uuid; + $num_rows = $this->database->select($sql, $parameters, 'column'); + if ($num_rows == 0) { + //built insert array + $array['menus'][0]['menu_uuid'] = $this->menu_uuid; + $array['menus'][0]['menu_name'] = 'default'; + $array['menus'][0]['menu_language'] = 'en-us'; + $array['menus'][0]['menu_description'] = 'Default Menu'; + + //grant temporary permissions + $p = permissions::new(); + $p->add('menu_add', 'temp'); + + //execute insert + $this->database->save($array); + unset($array); + + //revoke temporary permissions + $p->delete('menu_add', 'temp'); + + //add the menu items + $this->restore_default(); + } + } + + /** + * Restore the default menu and menu items. + * + * This method is used to restore the default menu and menu items when no menu exists in the database. + * It also ensures that the default user groups exist and are properly set up. + * + * This method will exit if an exception is thrown + */ + public function restore_default() { + + //get the $apps array from the installed apps from the core and mod directories + $config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_menu.php"); + $x = 0; + if (is_array($config_list)) { + foreach ($config_list as $config_path) { + $app_path = dirname($config_path); + $app_path = preg_replace('/\A.*(\/.*\/.*)\z/', '$1', $app_path); + $y = 0; + try { + //echo "[".$x ."] ".$config_path."\n"; + include($config_path); + $x++; + } catch (Exception $e) { + echo 'exception caught: ' . $e->getMessage() . "\n"; + exit; + } + } + } + + //get the list of languages + $language = new text; + + //create a uuid array of the original uuid used as the key and new uuid as the value + if (is_array($apps)) { + $x = 0; + foreach ($apps as $row) { + if (is_array($row['menu'])) { + foreach ($row['menu'] as $menu) { + $uuid_array[$menu['uuid']] = uuid(); + } + } + } + } + + //if the item uuid is not currently in the db then add it + $sql = "select * from v_menu_items "; + $sql .= "where menu_uuid = :menu_uuid "; + $parameters['menu_uuid'] = $this->menu_uuid; + $menu_items = $this->database->select($sql, $parameters, 'all'); + + //use the app array to restore the default menu + if (is_array($apps)) { + $x = 0; + foreach ($apps as $row) { + if (is_array($row['menu'])) { + foreach ($row['menu'] as $menu) { + //set the variables + if (!empty($menu['title'][$this->menu_language])) { + $menu_item_title = $menu['title'][$this->menu_language]; + } else { + $menu_item_title = $menu['title']['en-us']; + } + $uuid = $menu['uuid']; + $menu_item_uuid = $uuid_array[$menu['uuid']]; + $menu_item_parent_uuid = $uuid_array[$menu['parent_uuid']] ?? null; + $menu_item_category = $menu['category']; + $menu_item_icon = $menu['icon'] ?? null; + $menu_item_icon_color = $menu['icon_color'] ?? null; + $menu_item_path = $menu['path']; + $menu_item_order = $menu['order'] ?? null; + $menu_item_description = $menu['desc'] ?? null; + + //sanitize the menu link + $menu_item_path = preg_replace('#[^a-zA-Z0-9_:\-\.\&\=\?\/]#', '', $menu_item_path); + + //check if the menu item exists and if it does set the row array + $menu_item_exists = false; + foreach ($menu_items as $item) { + if ($item['uuid'] == $menu['uuid']) { + $menu_item_exists = true; + $row = $item; + } + } + + //item exists in the database + if ($menu_item_exists) { + $parent_menu_item_protected = 'false'; + //get parent_menu_item_protected + foreach ($menu_items as $item) { + if ($item['uuid'] == $menu['parent_uuid']) { + $parent_menu_item_protected = $item['menu_item_protected']; + } + } + + //parent is not protected so the parent uuid needs to be updated + if (is_uuid($menu_item_parent_uuid) && $menu_item_parent_uuid != $row['menu_item_parent_uuid'] && $parent_menu_item_protected != 'true') { + $array['menu_items'][$x]['menu_item_uuid'] = $row['menu_item_uuid']; + $array['menu_items'][$x]['menu_item_parent_uuid'] = $menu_item_parent_uuid; + $x++; + } + } + + //item does not exist in the database + if (!$menu_item_exists) { + if ($menu_item_uuid != $menu_item_parent_uuid) { + $array['menu_items'][$x]['menu_item_uuid'] = $menu_item_uuid; + $array['menu_items'][$x]['menu_uuid'] = $this->menu_uuid; + $array['menu_items'][$x]['uuid'] = $uuid; + $array['menu_items'][$x]['menu_item_title'] = $menu_item_title; + $array['menu_items'][$x]['menu_item_link'] = $menu_item_path; + $array['menu_items'][$x]['menu_item_category'] = $menu_item_category; + $array['menu_items'][$x]['menu_item_icon'] = $menu_item_icon; + $array['menu_items'][$x]['menu_item_icon_color'] = $menu_item_icon_color; + if (!empty($menu_item_order)) { + $array['menu_items'][$x]['menu_item_order'] = $menu_item_order; + } + if (is_uuid($menu_item_parent_uuid)) { + $array['menu_items'][$x]['menu_item_parent_uuid'] = $menu_item_parent_uuid; + } + $array['menu_items'][$x]['menu_item_description'] = $menu_item_description; + $x++; + } + } + unset($field, $parameters, $num_rows); + + //set the menu languages + if (!$menu_item_exists && is_array($language->languages)) { + foreach ($language->languages as $menu_language) { + //set the menu item title + if (!empty($menu["title"][$menu_language])) { + $menu_item_title = $menu["title"][$menu_language]; + } else { + $menu_item_title = $menu["title"]['en-us']; + } + + //build insert array + $array['menu_languages'][$x]['menu_language_uuid'] = uuid(); + $array['menu_languages'][$x]['menu_item_uuid'] = $menu_item_uuid; + $array['menu_languages'][$x]['menu_uuid'] = $this->menu_uuid; + $array['menu_languages'][$x]['menu_language'] = $menu_language; + $array['menu_languages'][$x]['menu_item_title'] = $menu_item_title; + $x++; + } + } + } + } + } + if (is_array($array) && @sizeof($array) != 0) { + //grant temporary permissions + $p = permissions::new(); + $p->add('menu_item_add', 'temp'); + $p->add('menu_language_add', 'temp'); + //execute insert + $this->database->save($array); + unset($array); + //revoke temporary permissions + $p->delete('menu_item_add', 'temp'); + $p->delete('menu_language_add', 'temp'); + } + } + + //make sure the default user groups exist + $group = new groups; + $group->defaults(); + + //get default global group_uuids + $sql = "select group_uuid, group_name from v_groups "; + $sql .= "where domain_uuid is null "; + $result = $this->database->select($sql, null, 'all'); + if (is_array($result) && @sizeof($result) != 0) { + foreach ($result as $row) { + $group_uuids[$row['group_name']] = $row['group_uuid']; + } + } + unset($sql, $result, $row); + + //if there are no groups listed in v_menu_item_groups under menu_item_uuid then add the default groups + if (is_array($apps)) { + $x = 0; + foreach ($apps as $app) { + if (is_array($apps)) { + foreach ($app['menu'] as $sub_row) { + if (isset($sub_row['groups'])) { + foreach ($sub_row['groups'] as $group) { + $sql = "select count(*) from v_menu_item_groups "; + $sql .= "where menu_item_uuid = :menu_item_uuid "; + $sql .= "and menu_uuid = :menu_uuid "; + $sql .= "and group_name = :group_name "; + $sql .= "and group_uuid = :group_uuid "; + $parameters['menu_item_uuid'] = $uuid_array[$sub_row['uuid']]; + $parameters['menu_uuid'] = $this->menu_uuid; + $parameters['group_name'] = $group; + $parameters['group_uuid'] = $group_uuids[$group] ?? null; + $num_rows = $this->database->select($sql, $parameters, 'column'); + if ($num_rows == 0) { + //no menu item groups found, build insert array for defaults + $array['menu_item_groups'][$x]['menu_item_group_uuid'] = uuid(); + $array['menu_item_groups'][$x]['menu_uuid'] = $this->menu_uuid; + $array['menu_item_groups'][$x]['menu_item_uuid'] = $uuid_array[$sub_row['uuid']]; + $array['menu_item_groups'][$x]['group_name'] = $group; + $array['menu_item_groups'][$x]['group_uuid'] = $group_uuids[$group] ?? null; + $x++; + } + unset($sql, $parameters, $num_rows); + } + } + } + } + } + + if (is_array($array) && @sizeof($array) != 0) { + //grant temporary permissions + $p = permissions::new(); + $p->add('menu_item_group_add', 'temp'); + //execute insert + $this->database->save($array); + unset($array); + //revoke temporary permissions + $p->delete('menu_item_group_add', 'temp'); + } } } + + /** + * build the fixed, static or inline horizontal menu html + * + * @param array $menu_array Associative array of menu items + * + * @return string HTML code for the generated menu. + */ + public function menu_horizontal($menu_array) { + + //determine menu behavior + $menu_style = $this->settings->get('theme', 'menu_style', 'fixed'); + switch ($menu_style) { + case 'inline': + $menu_type = 'default'; + $menu_width = 'calc(100% - 20px)'; + $menu_brand = false; + $menu_corners = null; + break; + case 'static': + $menu_type = 'static-top'; + $menu_width = 'calc(100% - 40px)'; + $menu_brand = true; + $menu_corners = "style='-webkit-border-radius: 0 0 4px 4px; -moz-border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;'"; + break; + case 'fixed': + default: + $menu_type = 'fixed-' . $this->settings->get('theme', 'menu_position', 'top'); + if (!http_user_agent('mobile')) { + $menu_width = $this->settings->get('theme', 'menu_width_fixed', 'calc(90% - 20px)'); + } + $menu_brand = true; + $menu_corners = null; + } + + //begin navbar code + $html = "\n"; + + //user menu on menu bar + //styles below are defined here to prevent caching (following a permission change, etc) + $html .= "\n"; + + $html .= "
    \n"; + $html .= "
    \n"; + if (!empty($_SESSION['user']['contact_image']) && is_uuid($_SESSION['user']['contact_image'])) { + $html .= "
    \n"; + } else { + $html .= "
    text['label-primary-contact-attachment-image'] . "\">
    \n"; + } + // $html .= "
    \n"; + $html .= "
    \n"; + if (!empty($_SESSION['user']['contact_name'])) { + $html .= "
    " . $_SESSION['user']['contact_name'] . "
    \n"; + } + if (!empty($_SESSION['user']['contact_organization'])) { + $html .= "
    " . $_SESSION['user']['contact_organization'] . "
    \n"; + } + if (!empty($_SESSION['user']['extension'][0]['destination'])) { + $html .= "
    " . $_SESSION['user']['extension'][0]['destination'] . "
    \n"; + } + $html .= "
    \n"; + $html .= " " . $this->text['title-user_profile'] . "
    \n"; + $html .= " " . $this->text['title-logout'] . "\n"; + $html .= "
    "; + $html .= "
    "; + $html .= "
    "; + $html .= "
    "; + + //modal for logout icon (above) + if (!empty($this->username) && $this->settings->get('theme', 'logout_icon_visible', 'false') == "true") { + $html .= modal::create(['id' => 'modal-logout', 'type' => 'general', 'message' => $this->text['theme-confirm-logout'], 'actions' => button::create(['type' => 'button', 'label' => $this->text['theme-label-logout'], 'icon' => 'fa-solid fa-right-from-bracket', 'id' => 'btn_logout', 'style' => 'float: right; margin-left: 15px;', 'collapse' => 'never', 'link' => PROJECT_PATH . '/logout.php', 'onclick' => "modal_close();"])]); + } + + return $html; + } + + /** + * Renders the vertical menu layout. + * + * @param array $menu_array An array of menu items. + * + * @return string The HTML markup for the vertical menu. + */ + public function menu_vertical($menu_array) { + //set defaults + $menu_side_state = $this->settings->get('theme', 'menu_side_state', 'contracted'); + $menu_side_state_class = $menu_side_state !== 'hidden' ? 'hide-sm-up ' : ''; + //menu brand image and/or text + $html = " \n"; + //main menu items + if (!empty($menu_array)) { + foreach ($menu_array as $menu_item_main) { + $menu_target = ($menu_item_main['menu_item_category'] == 'external') ? '_blank' : ''; + $html .= " "; + if (is_array($menu_item_main['menu_items']) && sizeof($menu_item_main['menu_items']) != 0 && $this->settings->get('theme', 'menu_side_item_main_sub_icons', true) === true) { + $html .= " \n"; + } + if (!empty($menu_item_main['menu_item_icon']) && substr($menu_item_main['menu_item_icon'], 0, 3) == 'fa-') { // font awesome icon + $html .= ""; + } + $html .= "" . $menu_item_main['menu_language_title'] . ""; + $html .= "\n"; + //sub menu items + if (is_array($menu_item_main['menu_items']) && sizeof($menu_item_main['menu_items']) != 0) { + $html .= " \n"; + } + } + $html .= "
    \n"; + } + $html .= "
    \n"; + $content_container_onclick = ""; + if ($menu_side_state != 'expanded') { + $content_container_onclick = "onclick=\"clearTimeout(menu_side_contract_timer); if ($(window).width() >= 576) { menu_side_contract(); }\""; + } + $html .= "
    \n"; + + //user menu on body header when side menu + //styles below are defined here to prevent caching (following a permission change, etc) + $html .= "\n"; + + $html .= "
    \n"; + $html .= "
    \n"; + if (!empty($_SESSION['user']['contact_image']) && is_uuid($_SESSION['user']['contact_image'])) { + $html .= "
    \n"; + } else { + $html .= "
    text['label-primary-contact-attachment-image'] . "\">
    \n"; + } + // $html .= "
    \n"; + $html .= "
    \n"; + if (!empty($_SESSION['user']['contact_name'])) { + $html .= "
    " . $_SESSION['user']['contact_name'] . "
    \n"; + } + if (!empty($_SESSION['user']['contact_organization'])) { + $html .= "
    " . $_SESSION['user']['contact_organization'] . "
    \n"; + } + if (!empty($_SESSION['user']['extension'][0]['destination'])) { + $html .= "
    " . $_SESSION['user']['extension'][0]['destination'] . "
    \n"; + } + $html .= "
    \n"; + $html .= " " . $this->text['title-user_profile'] . "
    \n"; + $html .= " " . $this->text['title-logout'] . "\n"; + $html .= "
    "; + $html .= "
    "; + $html .= "
    "; + $html .= "
    "; + + $html .= "
    \n"; + //header: left + $html .= "
    \n"; + // $html .= button::create(['type'=>'button','id'=>'menu_side_state_hidden_button','title'=>$this->text['theme-label-expand_menu'],'icon'=>'bars','class'=>'default '.($this->settings->get('theme', 'menu_side_state') != 'hidden' ? 'hide-sm-up ' : null).'float-left','onclick'=>'menu_side_expand();']); + $html .= "text['theme-label-expand_menu'] . "\">"; + $body_header_brand_text = escape($this->settings->get('theme', 'body_header_brand_text', 'FusionPBX')); + if ($this->settings->get('theme', 'body_header_brand_type') == 'image' || $this->settings->get('theme', 'body_header_brand_type') == 'image_text') { + $body_header_brand_image = $this->settings->get('theme', 'body_header_brand_image', PROJECT_PATH . '/themes/default/images/logo_side_expanded.png'); + $html .= "
    "; + $html .= ""; + $html .= "
    "; + } + if ($this->settings->get('theme', 'body_header_brand_type') == 'text' || $this->settings->get('theme', 'body_header_brand_type') == 'image_text') { + $html .= ""; + } + $html .= "
    \n"; + //header: right + $html .= "
    "; + //current user + //set (default) user graphic size and icon + $user_graphic_size = 18; + $user_graphic = ""; + //overwrite user graphic with image from session, if exists + if ($this->settings->get('theme', 'body_header_user_image', true) === true && !empty($_SESSION['user']['contact_image']) && is_uuid($_SESSION['user']['contact_image'])) { + $user_graphic_size = str_replace(['px', '%'], '', intval($this->settings->get('theme', 'body_header_user_image_size', 18))); + $user_graphic = " 18 ? '-' . (ceil(($user_graphic_size - 18) / 2) - 4) : '-4') . "px; background-image: url('" . PROJECT_PATH . "/core/contacts/contact_attachment.php?id=" . $_SESSION['user']['contact_image'] . "&action=download&sid=" . session_id() . "'); background-repeat: no-repeat; background-size: cover; background-position: center;\">"; + } + $html .= "\n"; + $html .= " username . "\" onclick=\"event.preventDefault(); $('#body_header_user_menu').toggleFadeSlide();\">" . ($user_graphic ?? null) . "" . escape($this->username) . ""; + $html .= "\n"; + //domain name/selector (sm+) + if (!empty($this->username) && permission_exists('domain_select') && count($_SESSION['domains']) > 1 && $this->settings->get('theme', 'domain_visible') == 'true') { + $html .= "\n"; + $html .= " " . escape($this->domain_name) . ""; + $html .= "\n"; + } + //logout icon + if (!empty($this->username) && $this->settings->get('theme', 'logout_icon_visible') == "true") { + $html .= "text['theme-label-logout'] . "\" onclick=\"modal_open('modal-logout','btn_logout');\">"; + } + $html .= "
    "; + $html .= "
    \n"; + + //modal for logout icon (above) + if (!empty($this->username) && $this->settings->get('theme', 'logout_icon_visible') == "true") { + $html .= modal::create(['id' => 'modal-logout', 'type' => 'general', 'message' => $this->text['theme-confirm-logout'], 'actions' => button::create(['type' => 'button', 'label' => $this->text['theme-label-logout'], 'icon' => 'fa-solid fa-right-from-bracket', 'id' => 'btn_logout', 'style' => 'float: right; margin-left: 15px;', 'collapse' => 'never', 'link' => PROJECT_PATH . '/logout.php', 'onclick' => "modal_close();"])]); + } + + return $html; + } + +} diff --git a/resources/classes/message.php b/resources/classes/message.php index 61e017753b..46f45ee6c9 100644 --- a/resources/classes/message.php +++ b/resources/classes/message.php @@ -25,48 +25,70 @@ Matthew Vale */ - class message { +class message { - static function add($message, $mood = null, $delay = null) { - //set mood and delay - $mood = $mood ?: 'positive'; - $delay = $delay ?: (1000 * (float) $_SESSION['theme']['message_delay']['text']); - //ignore duplicate messages - if (isset($_SESSION["messages"]) && !empty($_SESSION["messages"][$mood]['message'])) { - if (!in_array($message, $_SESSION["messages"][$mood]['message'])) { - $_SESSION["messages"][$mood]['message'][] = $message; - $_SESSION["messages"][$mood]['delay'][] = $delay; - } - } - else { - $_SESSION["messages"][$mood]['message'][] = $message; - $_SESSION["messages"][$mood]['delay'][] = $delay; - } - } + /** + * Returns the total number of messages in the session. + * + * @return int The number of messages, or 0 if no messages are present in the session. + */ + static function count() { + return isset($_SESSION["messages"]) && is_array($_SESSION["messages"]) ? sizeof($_SESSION["messages"]) : 0; + } - static function count() { - return isset($_SESSION["messages"]) && is_array($_SESSION["messages"]) ? sizeof($_SESSION["messages"]) : 0; - } - - static function html($clear_messages = true, $spacer = "") { - $html = "{$spacer}//render the messages\n"; - $spacer .="\t"; - if (isset($_SESSION['message']) || isset($_SESSION['messages'])) { - if (!empty($_SESSION['message']) && !is_array($_SESSION['message'])) { - self::add($_SESSION['message'], $_SESSION['message_mood'] ?? null, $_SESSION['message_delay'] ?? null); - unset($_SESSION['message'], $_SESSION['message_mood'], $_SESSION['message_delay']); - } - if (!empty($_SESSION['messages']) && is_array($_SESSION['messages']) && @sizeof($_SESSION['messages']) != 0) { - foreach ($_SESSION['messages'] as $message_mood => $message) { - $message_text = str_replace(array("\r\n", "\n", "\r"),'\\n',addslashes(join('
    ', $message['message']))); - $message_delay = array_sum($message['delay'])/count($message['delay']); - $html .= "{$spacer}display_message('$message_text', '$message_mood', '$message_delay');\n"; - } + /** + * Renders session messages into HTML. + * + * @param bool $clear_messages Whether to clear the session 'messages' array after rendering. Defaults to true. + * @param string $spacer The indentation spacer for the generated HTML code. + * + * @return string The rendered HTML message display code. + */ + static function html($clear_messages = true, $spacer = "") { + $html = "{$spacer}//render the messages\n"; + $spacer .= "\t"; + if (isset($_SESSION['message']) || isset($_SESSION['messages'])) { + if (!empty($_SESSION['message']) && !is_array($_SESSION['message'])) { + self::add($_SESSION['message'], $_SESSION['message_mood'] ?? null, $_SESSION['message_delay'] ?? null); + unset($_SESSION['message'], $_SESSION['message_mood'], $_SESSION['message_delay']); + } + if (!empty($_SESSION['messages']) && is_array($_SESSION['messages']) && @sizeof($_SESSION['messages']) != 0) { + foreach ($_SESSION['messages'] as $message_mood => $message) { + $message_text = str_replace(["\r\n", "\n", "\r"], '\\n', addslashes(join('
    ', $message['message']))); + $message_delay = array_sum($message['delay']) / count($message['delay']); + $html .= "{$spacer}display_message('$message_text', '$message_mood', '$message_delay');\n"; } } - if ($clear_messages) { - unset($_SESSION['messages']); + } + if ($clear_messages) { + unset($_SESSION['messages']); + } + return $html; + } + + /** + * Adds a message to the session messages array. + * + * @param string $message The message to add. + * @param string|null $mood The mood of the message. Defaults to 'positive'. + * @param int|null $delay The delay before displaying the message. Defaults to the theme's default text + * message delay in milliseconds. + * + * @return void + */ + static function add($message, $mood = null, $delay = null) { + //set mood and delay + $mood = $mood ?: 'positive'; + $delay = $delay ?: (1000 * (float)$_SESSION['theme']['message_delay']['text']); + //ignore duplicate messages + if (isset($_SESSION["messages"]) && !empty($_SESSION["messages"][$mood]['message'])) { + if (!in_array($message, $_SESSION["messages"][$mood]['message'])) { + $_SESSION["messages"][$mood]['message'][] = $message; + $_SESSION["messages"][$mood]['delay'][] = $delay; } - return $html; + } else { + $_SESSION["messages"][$mood]['message'][] = $message; + $_SESSION["messages"][$mood]['delay'][] = $delay; } } +} diff --git a/resources/classes/modal.php b/resources/classes/modal.php index 4fc244b27a..78a29a2719 100644 --- a/resources/classes/modal.php +++ b/resources/classes/modal.php @@ -25,52 +25,70 @@ Mark J Crane */ - class modal { +class modal { - static function create($array) { + /** + * Creates a modal window. + * + * @param array $array Array containing the modal's properties. + * The following keys are supported: + * - id: ID of the modal (optional). + * - type: Type of the modal (optional, one of 'copy', 'toggle', + * 'delete' or 'unassign'). If not specified, a general + * modal will be created. Defaults to 'general'. + * - title: Title of the modal (optional). If not specified, + * a default title will be used based on the type. + * - message: Message of the modal (optional). + * - actions: Actions of the modal (optional). + * - onclose: Function to call when the modal is closed + * (optional). + * + * @return string HTML string representing the modal window. + */ + static function create($array) { - //define as global - global $settings; + //define as global + global $settings; - //add multi-lingual support - $language = new text; - $text = $language->get(); + //add multi-lingual support + $language = new text; + $text = $language->get(); - $modal = ""; + + return $modal; } + +} diff --git a/resources/classes/parsedown.php b/resources/classes/parsedown.php index e9c3dc7dfc..e765cee580 100644 --- a/resources/classes/parsedown.php +++ b/resources/classes/parsedown.php @@ -13,1982 +13,2247 @@ # # -class parsedown -{ - # ~ - - const version = '1.8.0-beta-7'; - - # ~ - - function text($text) - { - $Elements = $this->textElements($text); - - # convert to markup - $markup = $this->elements($Elements); - - # trim line breaks - $markup = trim($markup, "\n"); - - return $markup; - } - - protected function textElements($text) - { - # make sure no definitions are set - $this->DefinitionData = array(); - - # standardize line breaks - $text = str_replace(array("\r\n", "\r"), "\n", $text); - - # remove surrounding line breaks - $text = trim($text, "\n"); - - # split text into lines - $lines = explode("\n", $text); - - # iterate through lines to identify blocks - return $this->linesElements($lines); - } - - # - # Setters - # - - function setBreaksEnabled($breaksEnabled) - { - $this->breaksEnabled = $breaksEnabled; - - return $this; - } - - protected $breaksEnabled; - - function setMarkupEscaped($markupEscaped) - { - $this->markupEscaped = $markupEscaped; - - return $this; - } - - protected $markupEscaped; - - function setUrlsLinked($urlsLinked) - { - $this->urlsLinked = $urlsLinked; - - return $this; - } - - protected $urlsLinked = true; - - function setSafeMode($safeMode) - { - $this->safeMode = (bool) $safeMode; - - return $this; - } - - protected $safeMode; - - function setStrictMode($strictMode) - { - $this->strictMode = (bool) $strictMode; - - return $this; - } - - protected $strictMode; - - protected $safeLinksWhitelist = array( - 'http://', - 'https://', - 'ftp://', - 'ftps://', - 'mailto:', - 'tel:', - 'data:image/png;base64,', - 'data:image/gif;base64,', - 'data:image/jpeg;base64,', - 'irc:', - 'ircs:', - 'git:', - 'ssh:', - 'news:', - 'steam:', - ); - - # - # Lines - # - - protected $BlockTypes = array( - '#' => array('Header'), - '*' => array('Rule', 'List'), - '+' => array('List'), - '-' => array('SetextHeader', 'Table', 'Rule', 'List'), - '0' => array('List'), - '1' => array('List'), - '2' => array('List'), - '3' => array('List'), - '4' => array('List'), - '5' => array('List'), - '6' => array('List'), - '7' => array('List'), - '8' => array('List'), - '9' => array('List'), - ':' => array('Table'), - '<' => array('Comment', 'Markup'), - '=' => array('SetextHeader'), - '>' => array('Quote'), - '[' => array('Reference'), - '_' => array('Rule'), - '`' => array('FencedCode'), - '|' => array('Table'), - '~' => array('FencedCode'), - ); - - # ~ - - protected $unmarkedBlockTypes = array( - 'Code', - ); - - # - # Blocks - # - - protected function lines(array $lines) - { - return $this->elements($this->linesElements($lines)); - } - - protected function linesElements(array $lines) - { - $Elements = array(); - $CurrentBlock = null; - - foreach ($lines as $line) - { - if (chop($line) === '') - { - if (isset($CurrentBlock)) - { - $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) - ? $CurrentBlock['interrupted'] + 1 : 1 - ); - } - - continue; - } - - while (($beforeTab = strstr($line, "\t", true)) !== false) - { - $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; - - $line = $beforeTab - . str_repeat(' ', $shortage) - . substr($line, strlen($beforeTab) + 1) - ; - } - - $indent = strspn($line, ' '); - - $text = $indent > 0 ? substr($line, $indent) : $line; - - # ~ - - $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); - - # ~ - - if (isset($CurrentBlock['continuable'])) - { - $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; - $Block = $this->$methodName($Line, $CurrentBlock); - - if (isset($Block)) - { - $CurrentBlock = $Block; - - continue; - } - else - { - if ($this->isBlockCompletable($CurrentBlock['type'])) - { - $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; - $CurrentBlock = $this->$methodName($CurrentBlock); - } - } - } - - # ~ - - $marker = $text[0]; - - # ~ - - $blockTypes = $this->unmarkedBlockTypes; - - if (isset($this->BlockTypes[$marker])) - { - foreach ($this->BlockTypes[$marker] as $blockType) - { - $blockTypes []= $blockType; - } - } - - # - # ~ - - foreach ($blockTypes as $blockType) - { - $Block = $this->{"block$blockType"}($Line, $CurrentBlock); - - if (isset($Block)) - { - $Block['type'] = $blockType; - - if ( ! isset($Block['identified'])) - { - if (isset($CurrentBlock)) - { - $Elements[] = $this->extractElement($CurrentBlock); - } - - $Block['identified'] = true; - } - - if ($this->isBlockContinuable($blockType)) - { - $Block['continuable'] = true; - } - - $CurrentBlock = $Block; - - continue 2; - } - } - - # ~ - - if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') - { - $Block = $this->paragraphContinue($Line, $CurrentBlock); - } - - if (isset($Block)) - { - $CurrentBlock = $Block; - } - else - { - if (isset($CurrentBlock)) - { - $Elements[] = $this->extractElement($CurrentBlock); - } - - $CurrentBlock = $this->paragraph($Line); - - $CurrentBlock['identified'] = true; - } - } - - # ~ - - if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) - { - $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; - $CurrentBlock = $this->$methodName($CurrentBlock); - } - - # ~ - - if (isset($CurrentBlock)) - { - $Elements[] = $this->extractElement($CurrentBlock); - } - - # ~ - - return $Elements; - } - - protected function extractElement(array $Component) - { - if ( ! isset($Component['element'])) - { - if (isset($Component['markup'])) - { - $Component['element'] = array('rawHtml' => $Component['markup']); - } - elseif (isset($Component['hidden'])) - { - $Component['element'] = array(); - } - } - - return $Component['element']; - } - - protected function isBlockContinuable($Type) - { - return method_exists($this, 'block' . $Type . 'Continue'); - } - - protected function isBlockCompletable($Type) - { - return method_exists($this, 'block' . $Type . 'Complete'); - } - - # - # Code - - protected function blockCode($Line, $Block = null) - { - if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) - { - return; - } - - if ($Line['indent'] >= 4) - { - $text = substr($Line['body'], 4); - - $Block = array( - 'element' => array( - 'name' => 'pre', - 'element' => array( - 'name' => 'code', - 'text' => $text, - ), - ), - ); - - return $Block; - } - } - - protected function blockCodeContinue($Line, $Block) - { - if ($Line['indent'] >= 4) - { - if (isset($Block['interrupted'])) - { - $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); - - unset($Block['interrupted']); - } - - $Block['element']['element']['text'] .= "\n"; - - $text = substr($Line['body'], 4); - - $Block['element']['element']['text'] .= $text; - - return $Block; - } - } - - protected function blockCodeComplete($Block) - { - return $Block; - } - - # - # Comment - - protected function blockComment($Line) - { - if ($this->markupEscaped or $this->safeMode) - { - return; - } - - if (strpos($Line['text'], '') !== false) - { - $Block['closed'] = true; - } - - return $Block; - } - } - - protected function blockCommentContinue($Line, array $Block) - { - if (isset($Block['closed'])) - { - return; - } - - $Block['element']['rawHtml'] .= "\n" . $Line['body']; - - if (strpos($Line['text'], '-->') !== false) - { - $Block['closed'] = true; - } - - return $Block; - } - - # - # Fenced Code - - protected function blockFencedCode($Line) - { - $marker = $Line['text'][0]; - - $openerLength = strspn($Line['text'], $marker); - - if ($openerLength < 3) - { - return; - } - - $infostring = trim(substr($Line['text'], $openerLength), "\t "); - - if (strpos($infostring, '`') !== false) - { - return; - } - - $Element = array( - 'name' => 'code', - 'text' => '', - ); - - if ($infostring !== '') - { - /** - * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes - * Every HTML element may have a class attribute specified. - * The attribute, if specified, must have a value that is a set - * of space-separated tokens representing the various classes - * that the element belongs to. - * [...] - * The space characters, for the purposes of this specification, - * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), - * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and - * U+000D CARRIAGE RETURN (CR). - */ - $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); - - $Element['attributes'] = array('class' => "language-$language"); - } - - $Block = array( - 'char' => $marker, - 'openerLength' => $openerLength, - 'element' => array( - 'name' => 'pre', - 'element' => $Element, - ), - ); - - return $Block; - } - - protected function blockFencedCodeContinue($Line, $Block) - { - if (isset($Block['complete'])) - { - return; - } - - if (isset($Block['interrupted'])) - { - $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); - - unset($Block['interrupted']); - } - - if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] - and chop(substr($Line['text'], $len), ' ') === '' - ) { - $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); - - $Block['complete'] = true; - - return $Block; - } - - $Block['element']['element']['text'] .= "\n" . $Line['body']; - - return $Block; - } - - protected function blockFencedCodeComplete($Block) - { - return $Block; - } - - # - # Header - - protected function blockHeader($Line) - { - $level = strspn($Line['text'], '#'); - - if ($level > 6) - { - return; - } - - $text = trim($Line['text'], '#'); - - if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') - { - return; - } - - $text = trim($text, ' '); - - $Block = array( - 'element' => array( - 'name' => 'h' . $level, - 'handler' => array( - 'function' => 'lineElements', - 'argument' => $text, - 'destination' => 'elements', - ) - ), - ); - - return $Block; - } - - # - # List - - protected function blockList($Line, array $CurrentBlock = null) - { - list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); - - if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) - { - $contentIndent = strlen($matches[2]); - - if ($contentIndent >= 5) - { - $contentIndent -= 1; - $matches[1] = substr($matches[1], 0, -$contentIndent); - $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; - } - elseif ($contentIndent === 0) - { - $matches[1] .= ' '; - } - - $markerWithoutWhitespace = strstr($matches[1], ' ', true); - - $Block = array( - 'indent' => $Line['indent'], - 'pattern' => $pattern, - 'data' => array( - 'type' => $name, - 'marker' => $matches[1], - 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), - ), - 'element' => array( - 'name' => $name, - 'elements' => array(), - ), - ); - $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); - - if ($name === 'ol') - { - $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; - - if ($listStart !== '1') - { - if ( - isset($CurrentBlock) - and $CurrentBlock['type'] === 'Paragraph' - and ! isset($CurrentBlock['interrupted']) - ) { - return; - } - - $Block['element']['attributes'] = array('start' => $listStart); - } - } - - $Block['li'] = array( - 'name' => 'li', - 'handler' => array( - 'function' => 'li', - 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), - 'destination' => 'elements' - ) - ); - - $Block['element']['elements'] []= & $Block['li']; - - return $Block; - } - } - - protected function blockListContinue($Line, array $Block) - { - if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) - { - return null; - } - - $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); - - if ($Line['indent'] < $requiredIndent - and ( - ( - $Block['data']['type'] === 'ol' - and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) - ) or ( - $Block['data']['type'] === 'ul' - and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) - ) - ) - ) { - if (isset($Block['interrupted'])) - { - $Block['li']['handler']['argument'] []= ''; - - $Block['loose'] = true; - - unset($Block['interrupted']); - } - - unset($Block['li']); - - $text = isset($matches[1]) ? $matches[1] : ''; - - $Block['indent'] = $Line['indent']; - - $Block['li'] = array( - 'name' => 'li', - 'handler' => array( - 'function' => 'li', - 'argument' => array($text), - 'destination' => 'elements' - ) - ); - - $Block['element']['elements'] []= & $Block['li']; - - return $Block; - } - elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) - { - return null; - } - - if ($Line['text'][0] === '[' and $this->blockReference($Line)) - { - return $Block; - } - - if ($Line['indent'] >= $requiredIndent) - { - if (isset($Block['interrupted'])) - { - $Block['li']['handler']['argument'] []= ''; - - $Block['loose'] = true; - - unset($Block['interrupted']); - } - - $text = substr($Line['body'], $requiredIndent); - - $Block['li']['handler']['argument'] []= $text; - - return $Block; - } - - if ( ! isset($Block['interrupted'])) - { - $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); - - $Block['li']['handler']['argument'] []= $text; - - return $Block; - } - } - - protected function blockListComplete(array $Block) - { - if (isset($Block['loose'])) - { - foreach ($Block['element']['elements'] as &$li) - { - if (end($li['handler']['argument']) !== '') - { - $li['handler']['argument'] []= ''; - } - } - } - - return $Block; - } - - # - # Quote - - protected function blockQuote($Line) - { - if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) - { - $Block = array( - 'element' => array( - 'name' => 'blockquote', - 'handler' => array( - 'function' => 'linesElements', - 'argument' => (array) $matches[1], - 'destination' => 'elements', - ) - ), - ); - - return $Block; - } - } - - protected function blockQuoteContinue($Line, array $Block) - { - if (isset($Block['interrupted'])) - { - return; - } - - if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) - { - $Block['element']['handler']['argument'] []= $matches[1]; - - return $Block; - } - - if ( ! isset($Block['interrupted'])) - { - $Block['element']['handler']['argument'] []= $Line['text']; - - return $Block; - } - } - - # - # Rule - - protected function blockRule($Line) - { - $marker = $Line['text'][0]; - - if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') - { - $Block = array( - 'element' => array( - 'name' => 'hr', - ), - ); - - return $Block; - } - } - - # - # Setext - - protected function blockSetextHeader($Line, array $Block = null) - { - if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) - { - return; - } - - if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') - { - $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; - - return $Block; - } - } - - # - # Markup - - protected function blockMarkup($Line) - { - if ($this->markupEscaped or $this->safeMode) - { - return; - } - - if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) - { - $element = strtolower($matches[1]); - - if (in_array($element, $this->textLevelElements)) - { - return; - } - - $Block = array( - 'name' => $matches[1], - 'element' => array( - 'rawHtml' => $Line['text'], - 'autobreak' => true, - ), - ); - - return $Block; - } - } - - protected function blockMarkupContinue($Line, array $Block) - { - if (isset($Block['closed']) or isset($Block['interrupted'])) - { - return; - } - - $Block['element']['rawHtml'] .= "\n" . $Line['body']; - - return $Block; - } - - # - # Reference - - protected function blockReference($Line) - { - if (strpos($Line['text'], ']') !== false - and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) - ) { - $id = strtolower($matches[1]); - - $Data = array( - 'url' => $matches[2], - 'title' => isset($matches[3]) ? $matches[3] : null, - ); - - $this->DefinitionData['Reference'][$id] = $Data; - - $Block = array( - 'element' => array(), - ); - - return $Block; - } - } - - # - # Table - - protected function blockTable($Line, array $Block = null) - { - if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) - { - return; - } - - if ( - strpos($Block['element']['handler']['argument'], '|') === false - and strpos($Line['text'], '|') === false - and strpos($Line['text'], ':') === false - or strpos($Block['element']['handler']['argument'], "\n") !== false - ) { - return; - } - - if (chop($Line['text'], ' -:|') !== '') - { - return; - } - - $alignments = array(); - - $divider = $Line['text']; - - $divider = trim($divider); - $divider = trim($divider, '|'); - - $dividerCells = explode('|', $divider); - - foreach ($dividerCells as $dividerCell) - { - $dividerCell = trim($dividerCell); - - if ($dividerCell === '') - { - return; - } - - $alignment = null; - - if ($dividerCell[0] === ':') - { - $alignment = 'left'; - } - - if (substr($dividerCell, - 1) === ':') - { - $alignment = $alignment === 'left' ? 'center' : 'right'; - } - - $alignments []= $alignment; - } - - # ~ - - $HeaderElements = array(); - - $header = $Block['element']['handler']['argument']; - - $header = trim($header); - $header = trim($header, '|'); - - $headerCells = explode('|', $header); - - if (count($headerCells) !== count($alignments)) - { - return; - } - - foreach ($headerCells as $index => $headerCell) - { - $headerCell = trim($headerCell); - - $HeaderElement = array( - 'name' => 'th', - 'handler' => array( - 'function' => 'lineElements', - 'argument' => $headerCell, - 'destination' => 'elements', - ) - ); - - if (isset($alignments[$index])) - { - $alignment = $alignments[$index]; - - $HeaderElement['attributes'] = array( - 'style' => "text-align: $alignment;", - ); - } - - $HeaderElements []= $HeaderElement; - } - - # ~ - - $Block = array( - 'alignments' => $alignments, - 'identified' => true, - 'element' => array( - 'name' => 'table', - 'elements' => array(), - ), - ); - - $Block['element']['elements'] []= array( - 'name' => 'thead', - ); - - $Block['element']['elements'] []= array( - 'name' => 'tbody', - 'elements' => array(), - ); - - $Block['element']['elements'][0]['elements'] []= array( - 'name' => 'tr', - 'elements' => $HeaderElements, - ); - - return $Block; - } - - protected function blockTableContinue($Line, array $Block) - { - if (isset($Block['interrupted'])) - { - return; - } - - if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) - { - $Elements = array(); - - $row = $Line['text']; - - $row = trim($row); - $row = trim($row, '|'); - - preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); - - $cells = array_slice($matches[0], 0, count($Block['alignments'])); - - foreach ($cells as $index => $cell) - { - $cell = trim($cell); - - $Element = array( - 'name' => 'td', - 'handler' => array( - 'function' => 'lineElements', - 'argument' => $cell, - 'destination' => 'elements', - ) - ); - - if (isset($Block['alignments'][$index])) - { - $Element['attributes'] = array( - 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', - ); - } - - $Elements []= $Element; - } - - $Element = array( - 'name' => 'tr', - 'elements' => $Elements, - ); - - $Block['element']['elements'][1]['elements'] []= $Element; - - return $Block; - } - } - - # - # ~ - # - - protected function paragraph($Line) - { - return array( - 'type' => 'Paragraph', - 'element' => array( - 'name' => 'p', - 'handler' => array( - 'function' => 'lineElements', - 'argument' => $Line['text'], - 'destination' => 'elements', - ), - ), - ); - } - - protected function paragraphContinue($Line, array $Block) - { - if (isset($Block['interrupted'])) - { - return; - } - - $Block['element']['handler']['argument'] .= "\n".$Line['text']; - - return $Block; - } - - # - # Inline Elements - # - - protected $InlineTypes = array( - '!' => array('Image'), - '&' => array('SpecialCharacter'), - '*' => array('Emphasis'), - ':' => array('Url'), - '<' => array('UrlTag', 'EmailTag', 'Markup'), - '[' => array('Link'), - '_' => array('Emphasis'), - '`' => array('Code'), - '~' => array('Strikethrough'), - '\\' => array('EscapeSequence'), - ); - - # ~ - - protected $inlineMarkerList = '!*_&[:<`~\\'; - - # - # ~ - # - - public function line($text, $nonNestables = array()) - { - return $this->elements($this->lineElements($text, $nonNestables)); - } - - protected function lineElements($text, $nonNestables = array()) - { - # standardize line breaks - $text = str_replace(array("\r\n", "\r"), "\n", $text); - - $Elements = array(); - - $nonNestables = (empty($nonNestables) - ? array() - : array_combine($nonNestables, $nonNestables) - ); - - # $excerpt is based on the first occurrence of a marker - - while ($excerpt = strpbrk($text, $this->inlineMarkerList)) - { - $marker = $excerpt[0]; - - $markerPosition = strlen($text) - strlen($excerpt); - - $Excerpt = array('text' => $excerpt, 'context' => $text); - - foreach ($this->InlineTypes[$marker] as $inlineType) - { - # check to see if the current inline type is nestable in the current context - - if (isset($nonNestables[$inlineType])) - { - continue; - } - - $Inline = $this->{"inline$inlineType"}($Excerpt); - - if ( ! isset($Inline)) - { - continue; - } - - # makes sure that the inline belongs to "our" marker - - if (isset($Inline['position']) and $Inline['position'] > $markerPosition) - { - continue; - } - - # sets a default inline position - - if ( ! isset($Inline['position'])) - { - $Inline['position'] = $markerPosition; - } - - # cause the new element to 'inherit' our non nestables - - - $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) - ? array_merge($Inline['element']['nonNestables'], $nonNestables) - : $nonNestables - ; - - # the text that comes before the inline - $unmarkedText = substr($text, 0, $Inline['position']); - - # compile the unmarked text - $InlineText = $this->inlineText($unmarkedText); - $Elements[] = $InlineText['element']; - - # compile the inline - $Elements[] = $this->extractElement($Inline); - - # remove the examined text - $text = substr($text, $Inline['position'] + $Inline['extent']); - - continue 2; - } - - # the marker does not belong to an inline - - $unmarkedText = substr($text, 0, $markerPosition + 1); - - $InlineText = $this->inlineText($unmarkedText); - $Elements[] = $InlineText['element']; - - $text = substr($text, $markerPosition + 1); - } - - $InlineText = $this->inlineText($text); - $Elements[] = $InlineText['element']; - - foreach ($Elements as &$Element) - { - if ( ! isset($Element['autobreak'])) - { - $Element['autobreak'] = false; - } - } - - return $Elements; - } - - # - # ~ - # - - protected function inlineText($text) - { - $Inline = array( - 'extent' => strlen($text), - 'element' => array(), - ); - - $Inline['element']['elements'] = self::pregReplaceElements( - $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', - array( - array('name' => 'br'), - array('text' => "\n"), - ), - $text - ); - - return $Inline; - } - - protected function inlineCode($Excerpt) - { - $marker = $Excerpt['text'][0]; - - if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), - 'element' => array( - 'name' => 'code', - 'text' => $text, - ), - ); - } - } - - protected function inlineEmailTag($Excerpt) - { - $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; - - $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' - . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; - - if (strpos($Excerpt['text'], '>') !== false - and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) - ){ - $url = $matches[1]; - - if ( ! isset($matches[2])) - { - $url = "mailto:$url"; - } - - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => 'a', - 'text' => $matches[1], - 'attributes' => array( - 'href' => $url, - ), - ), - ); - } - } - - protected function inlineEmphasis($Excerpt) - { - if ( ! isset($Excerpt['text'][1])) - { - return; - } - - $marker = $Excerpt['text'][0]; - - if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) - { - $emphasis = 'strong'; - } - elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) - { - $emphasis = 'em'; - } - else - { - return; - } - - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => $emphasis, - 'handler' => array( - 'function' => 'lineElements', - 'argument' => $matches[1], - 'destination' => 'elements', - ) - ), - ); - } - - protected function inlineEscapeSequence($Excerpt) - { - if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) - { - return array( - 'element' => array('rawHtml' => $Excerpt['text'][1]), - 'extent' => 2, - ); - } - } - - protected function inlineImage($Excerpt) - { - if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') - { - return; - } - - $Excerpt['text']= substr($Excerpt['text'], 1); - - $Link = $this->inlineLink($Excerpt); - - if ($Link === null) - { - return; - } - - $Inline = array( - 'extent' => $Link['extent'] + 1, - 'element' => array( - 'name' => 'img', - 'attributes' => array( - 'src' => $Link['element']['attributes']['href'], - 'alt' => $Link['element']['handler']['argument'], - ), - 'autobreak' => true, - ), - ); - - $Inline['element']['attributes'] += $Link['element']['attributes']; - - unset($Inline['element']['attributes']['href']); - - return $Inline; - } - - protected function inlineLink($Excerpt) - { - $Element = array( - 'name' => 'a', - 'handler' => array( - 'function' => 'lineElements', - 'argument' => null, - 'destination' => 'elements', - ), - 'nonNestables' => array('Url', 'Link'), - 'attributes' => array( - 'href' => null, - 'title' => null, - ), - ); - - $extent = 0; - - $remainder = $Excerpt['text']; - - if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) - { - $Element['handler']['argument'] = $matches[1]; - - $extent += strlen($matches[0]); - - $remainder = substr($remainder, $extent); - } - else - { - return; - } - - if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) - { - $Element['attributes']['href'] = $matches[1]; - - if (isset($matches[2])) - { - $Element['attributes']['title'] = substr($matches[2], 1, - 1); - } - - $extent += strlen($matches[0]); - } - else - { - if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) - { - $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; - $definition = strtolower($definition); - - $extent += strlen($matches[0]); - } - else - { - $definition = strtolower($Element['handler']['argument']); - } - - if ( ! isset($this->DefinitionData['Reference'][$definition])) - { - return; - } - - $Definition = $this->DefinitionData['Reference'][$definition]; - - $Element['attributes']['href'] = $Definition['url']; - $Element['attributes']['title'] = $Definition['title']; - } - - return array( - 'extent' => $extent, - 'element' => $Element, - ); - } - - protected function inlineMarkup($Excerpt) - { - if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) - { - return; - } - - if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) - { - return array( - 'element' => array('rawHtml' => $matches[0]), - 'extent' => strlen($matches[0]), - ); - } - - if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) - { - return array( - 'element' => array('rawHtml' => $matches[0]), - 'extent' => strlen($matches[0]), - ); - } - - if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) - { - return array( - 'element' => array('rawHtml' => $matches[0]), - 'extent' => strlen($matches[0]), - ); - } - } - - protected function inlineSpecialCharacter($Excerpt) - { - if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false - and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) - ) { - return array( - 'element' => array('rawHtml' => '&' . $matches[1] . ';'), - 'extent' => strlen($matches[0]), - ); - } - - return; - } - - protected function inlineStrikethrough($Excerpt) - { - if ( ! isset($Excerpt['text'][1])) - { - return; - } - - if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) - { - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => 'del', - 'handler' => array( - 'function' => 'lineElements', - 'argument' => $matches[1], - 'destination' => 'elements', - ) - ), - ); - } - } - - protected function inlineUrl($Excerpt) - { - if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') - { - return; - } - - if (strpos($Excerpt['context'], 'http') !== false - and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) - ) { - $url = $matches[0][0]; - - $Inline = array( - 'extent' => strlen($matches[0][0]), - 'position' => $matches[0][1], - 'element' => array( - 'name' => 'a', - 'text' => $url, - 'attributes' => array( - 'href' => $url, - ), - ), - ); - - return $Inline; - } - } - - protected function inlineUrlTag($Excerpt) - { - if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) - { - $url = $matches[1]; - - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => 'a', - 'text' => $url, - 'attributes' => array( - 'href' => $url, - ), - ), - ); - } - } - - # ~ - - protected function unmarkedText($text) - { - $Inline = $this->inlineText($text); - return $this->element($Inline['element']); - } - - # - # Handlers - # - - protected function handle(array $Element) - { - if (isset($Element['handler'])) - { - if (!isset($Element['nonNestables'])) - { - $Element['nonNestables'] = array(); - } - - if (is_string($Element['handler'])) - { - $function = $Element['handler']; - $argument = $Element['text']; - unset($Element['text']); - $destination = 'rawHtml'; - } - else - { - $function = $Element['handler']['function']; - $argument = $Element['handler']['argument']; - $destination = $Element['handler']['destination']; - } - - $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); - - if ($destination === 'handler') - { - $Element = $this->handle($Element); - } - - unset($Element['handler']); - } - - return $Element; - } - - protected function handleElementRecursive(array $Element) - { - return $this->elementApplyRecursive(array($this, 'handle'), $Element); - } - - protected function handleElementsRecursive(array $Elements) - { - return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); - } - - protected function elementApplyRecursive($closure, array $Element) - { - $Element = call_user_func($closure, $Element); - - if (isset($Element['elements'])) - { - $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); - } - elseif (isset($Element['element'])) - { - $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); - } - - return $Element; - } - - protected function elementApplyRecursiveDepthFirst($closure, array $Element) - { - if (isset($Element['elements'])) - { - $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); - } - elseif (isset($Element['element'])) - { - $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); - } - - $Element = call_user_func($closure, $Element); - - return $Element; - } - - protected function elementsApplyRecursive($closure, array $Elements) - { - foreach ($Elements as &$Element) - { - $Element = $this->elementApplyRecursive($closure, $Element); - } - - return $Elements; - } - - protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) - { - foreach ($Elements as &$Element) - { - $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); - } - - return $Elements; - } - - protected function element(array $Element) - { - if ($this->safeMode) - { - $Element = $this->sanitiseElement($Element); - } - - # identity map if element has no handler - $Element = $this->handle($Element); - - $hasName = isset($Element['name']); - - $markup = ''; - - if ($hasName) - { - $markup .= '<' . $Element['name']; - - if (isset($Element['attributes'])) - { - foreach ($Element['attributes'] as $name => $value) - { - if ($value === null) - { - continue; - } - - $markup .= " $name=\"".self::escape($value).'"'; - } - } - } - - $permitRawHtml = false; - - if (isset($Element['text'])) - { - $text = $Element['text']; - } - // very strongly consider an alternative if you're writing an - // extension - elseif (isset($Element['rawHtml'])) - { - $text = $Element['rawHtml']; - - $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; - $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; - } - - $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); - - if ($hasContent) - { - $markup .= $hasName ? '>' : ''; - - if (isset($Element['elements'])) - { - $markup .= $this->elements($Element['elements']); - } - elseif (isset($Element['element'])) - { - $markup .= $this->element($Element['element']); - } - else - { - if (!$permitRawHtml) - { - $markup .= self::escape($text, true); - } - else - { - $markup .= $text; - } - } - - $markup .= $hasName ? '' : ''; - } - elseif ($hasName) - { - $markup .= ' />'; - } - - return $markup; - } - - protected function elements(array $Elements) - { - $markup = ''; - - $autoBreak = true; - - foreach ($Elements as $Element) - { - if (empty($Element)) - { - continue; - } - - $autoBreakNext = (isset($Element['autobreak']) - ? $Element['autobreak'] : isset($Element['name']) - ); - // (autobreak === false) covers both sides of an element - $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; - - $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); - $autoBreak = $autoBreakNext; - } - - $markup .= $autoBreak ? "\n" : ''; - - return $markup; - } - - # ~ - - protected function li($lines) - { - $Elements = $this->linesElements($lines); - - if ( ! in_array('', $lines) - and isset($Elements[0]) and isset($Elements[0]['name']) - and $Elements[0]['name'] === 'p' - ) { - unset($Elements[0]['name']); - } - - return $Elements; - } - - # - # AST Convenience - # - - /** - * Replace occurrences $regexp with $Elements in $text. Return an array of - * elements representing the replacement. - */ - protected static function pregReplaceElements($regexp, $Elements, $text) - { - $newElements = array(); - - while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) - { - $offset = $matches[0][1]; - $before = substr($text, 0, $offset); - $after = substr($text, $offset + strlen($matches[0][0])); - - $newElements[] = array('text' => $before); - - foreach ($Elements as $Element) - { - $newElements[] = $Element; - } - - $text = $after; - } - - $newElements[] = array('text' => $text); - - return $newElements; - } - - # - # Deprecated Methods - # - - function parse($text) - { - $markup = $this->text($text); - - return $markup; - } - - protected function sanitiseElement(array $Element) - { - static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; - static $safeUrlNameToAtt = array( - 'a' => 'href', - 'img' => 'src', - ); - - if ( ! isset($Element['name'])) - { - unset($Element['attributes']); - return $Element; - } - - if (isset($safeUrlNameToAtt[$Element['name']])) - { - $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); - } - - if ( ! empty($Element['attributes'])) - { - foreach ($Element['attributes'] as $att => $val) - { - # filter out badly parsed attribute - if ( ! preg_match($goodAttribute, $att)) - { - unset($Element['attributes'][$att]); - } - # dump onevent attribute - elseif (self::striAtStart($att, 'on')) - { - unset($Element['attributes'][$att]); - } - } - } - - return $Element; - } - - protected function filterUnsafeUrlInAttribute(array $Element, $attribute) - { - foreach ($this->safeLinksWhitelist as $scheme) - { - if (self::striAtStart($Element['attributes'][$attribute], $scheme)) - { - return $Element; - } - } - - $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); - - return $Element; - } - - # - # Static Methods - # - - protected static function escape($text, $allowQuotes = false) - { - return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); - } - - protected static function striAtStart($string, $needle) - { - $len = strlen($needle); - - if ($len > strlen($string)) - { - return false; - } - else - { - return strtolower(substr($string, 0, $len)) === strtolower($needle); - } - } - - static function instance($name = 'default') - { - if (isset(self::$instances[$name])) - { - return self::$instances[$name]; - } - - $instance = new static(); - - self::$instances[$name] = $instance; - - return $instance; - } - - private static $instances = array(); - - # - # Fields - # - - protected $DefinitionData; - - # - # Read-Only - - protected $specialCharacters = array( - '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' - ); - - protected $StrongRegex = array( - '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', - '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', - ); - - protected $EmRegex = array( - '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', - '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', - ); - - protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; - - protected $voidElements = array( - 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', - ); - - protected $textLevelElements = array( - 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', - 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', - 'i', 'rp', 'del', 'code', 'strike', 'marquee', - 'q', 'rt', 'ins', 'font', 'strong', - 's', 'tt', 'kbd', 'mark', - 'u', 'xm', 'sub', 'nobr', - 'sup', 'ruby', - 'var', 'span', - 'wbr', 'time', - ); +class parsedown { + # ~ + + const version = '1.8.0-beta-7'; + + # ~ + private static $instances = []; + protected $breaksEnabled; + + # + # Setters + # + protected $markupEscaped; + protected $urlsLinked = true; + protected $safeMode; + protected $strictMode; + protected $safeLinksWhitelist = [ + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ]; + protected $BlockTypes = [ + '#' => ['Header'], + '*' => ['Rule', 'List'], + '+' => ['List'], + '-' => ['SetextHeader', 'Table', 'Rule', 'List'], + '0' => ['List'], + '1' => ['List'], + '2' => ['List'], + '3' => ['List'], + '4' => ['List'], + '5' => ['List'], + '6' => ['List'], + '7' => ['List'], + '8' => ['List'], + '9' => ['List'], + ':' => ['Table'], + '<' => ['Comment', 'Markup'], + '=' => ['SetextHeader'], + '>' => ['Quote'], + '[' => ['Reference'], + '_' => ['Rule'], + '`' => ['FencedCode'], + '|' => ['Table'], + '~' => ['FencedCode'], + ]; + protected $unmarkedBlockTypes = [ + 'Code', + ]; + protected $InlineTypes = [ + '!' => ['Image'], + '&' => ['SpecialCharacter'], + '*' => ['Emphasis'], + ':' => ['Url'], + '<' => ['UrlTag', 'EmailTag', 'Markup'], + '[' => ['Link'], + '_' => ['Emphasis'], + '`' => ['Code'], + '~' => ['Strikethrough'], + '\\' => ['EscapeSequence'], + ]; + protected $inlineMarkerList = '!*_&[:<`~\\'; + protected $DefinitionData; + protected $specialCharacters = [ + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~', + ]; + + # + # Lines + # + protected $StrongRegex = [ + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ]; + + # ~ + protected $EmRegex = [ + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ]; + + # + # Blocks + # + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + protected $voidElements = [ + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ]; + protected $textLevelElements = [ + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ]; + + /** + * Returns an instance of the class, potentially creating a new one if it does not already exist. + * + * @param string $name The name to identify the instance by. Defaults to 'default'. + * + * @return static The created or retrieved instance of the class + */ + static function instance($name = 'default') { + if (isset(self::$instances[$name])) { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + /** + * Enable or disable breaks in the current object. Return the object instance for + * method chaining. + * + * @param boolean $breaksEnabled Whether to enable or disable breaks. + * + * @return self The current object instance. + */ + function setBreaksEnabled($breaksEnabled) { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + # + # Code + + /** + * Set whether markup should be escaped or not. + * + * @param bool $markupEscaped Flag to indicate whether markup should be escaped. + * + * @return self This instance for method chaining. + */ + function setMarkupEscaped($markupEscaped) { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + /** + * Sets an array of URLs that will be linked together. + * + * @param array $urlsLinked An array of URLs to link together + * + * @return self The instance of the class for chaining method calls + */ + function setUrlsLinked($urlsLinked) { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + /** + * Sets whether safe mode should be enabled or not. + * + * @param bool $safeMode Whether to enable safe mode + * + * @return self The instance of the class for chaining method calls + */ + function setSafeMode($safeMode) { + $this->safeMode = (bool)$safeMode; + + return $this; + } + + # + # Comment + + /** + * Sets the strict mode flag. + * + * @param bool $strictMode The new value for the strict mode flag + * + * @return self The instance of the class for chaining method calls + */ + function setStrictMode($strictMode) { + $this->strictMode = (bool)$strictMode; + + return $this; + } + + /** + * Adds a new text element to the current context with the specified text and non-nestable elements. + * + * @param string $text The text of the new line element + * @param array $nonNestables An array of elements that cannot be nested within other elements + * + * @return self The instance of the class for chaining method calls + */ + public function line($text, $nonNestables = []) { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + # + # Fenced Code + + /** + * Extracts line elements from the given text. + * + * @param string $text The input text to process + * @param array $nonNestables An array of non-nestable inline types + * + * @return array An array of extracted line elements + */ + protected function lineElements($text, $nonNestables = []) { + # standardize line breaks + $text = str_replace(["\r\n", "\r"], "\n", $text); + + $Elements = []; + + $nonNestables = (empty($nonNestables) + ? [] + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = ['text' => $excerpt, 'context' => $text]; + + foreach ($this->InlineTypes[$marker] as $inlineType) { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if (!isset($Inline)) { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) { + continue; + } + + # sets a default inline position + + if (!isset($Inline['position'])) { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) { + if (!isset($Element['autobreak'])) { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + /** + * Creates an inline element with the given text. + * + * @param string $text The text to be processed for inline elements. + * + * @return array An array containing information about the inline element, including its extent and child elements. + */ + protected function inlineText($text) { + $Inline = [ + 'extent' => strlen($text), + 'element' => [], + ]; + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + [ + ['name' => 'br'], + ['text' => "\n"], + ], + $text + ); + + return $Inline; + } + + /** + * Replaces elements in a text using regular expressions and an array of elements to insert. + * + * This method iterates through the input text, matches the provided regular expression, + * and replaces the matched element with the provided elements inserted into the captured text segments. + * + * @param string $regexp The regular expression to match in the text + * @param array $Elements An array of elements to be inserted after each matched element + * @param string $text The input text to replace elements in + * + * @return array A new array of elements containing the replaced text with the provided elements inserted + */ + protected static function pregReplaceElements($regexp, $Elements, $text) { + $newElements = []; + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = ['text' => $before]; + + foreach ($Elements as $Element) { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = ['text' => $text]; + + return $newElements; + } + + # + # Header + + /** + * Parses the given text and returns its markup representation. + * + * @param string $text The text to be parsed + * + * @return object The markup representation of the parsed text + */ + function parse($text) { + $markup = $this->text($text); + + return $markup; + } + + # + # List + + /** + * Convert text into markup format. This function trims line breaks and replaces + * elements with their markup representations. + * + * @param string $text The text to be converted. + * + * @return string The markup representation of the input text. + */ + function text($text) { + $Elements = $this->textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + /** + * Convert a block of text into an array of elements representing the structure of the text. + * + * This method standardizes line breaks, removes surrounding line breaks, splits the text into lines, + * and then identifies blocks within those lines. The result is an array of elements that can be + * further processed to extract specific information from the text. + * + * @param string $text The input text to convert into elements. + * + * @return array An array of elements representing the structure of the input text. + */ + protected function textElements($text) { + # make sure no definitions are set + $this->DefinitionData = []; + + # standardize line breaks + $text = str_replace(["\r\n", "\r"], "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + /** + * Processes an array of lines and returns a collection of elements. + * + * @param array $lines An array of lines to process + * + * @return self The instance of the class for chaining method calls + */ + protected function lines(array $lines) { + return $this->elements($this->linesElements($lines)); + } + + # + # Quote + + /** + * Renders a list of elements as markup. + * + * @param array $Elements An array of element data + * Each element should be an associative array with 'name' and/or 'autobreak' keys + * + * @return string The rendered markup + */ + protected function elements(array $Elements) { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) { + if (empty($Element)) { + continue; + } + + $autoBreakNext = (isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + /** + * Renders an HTML element. + * + * @param array $Element An associative array describing the element to render, including its attributes and + * content. + * + * @return string The rendered markup for the given element. + */ + protected function element(array $Element) { + if ($this->safeMode) { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) { + foreach ($Element['attributes'] as $name => $value) { + if ($value === null) { + continue; + } + + $markup .= " $name=\"" . self::escape($value) . '"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) { + $markup .= $this->elements($Element['elements']); + } elseif (isset($Element['element'])) { + $markup .= $this->element($Element['element']); + } else { + if (!$permitRawHtml) { + $markup .= self::escape($text, true); + } else { + $markup .= $text; + } + } + + $markup .= $hasName ? '' : ''; + } elseif ($hasName) { + $markup .= ' />'; + } + + return $markup; + } + + # + # Rule + + /** + * Sanitizes an element by filtering out invalid attributes and preventing XSS attacks. + * + * @param array $Element The HTML element to sanitize + * + * @return array The sanitized element + */ + protected function sanitiseElement(array $Element) { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = [ + 'a' => 'href', + 'img' => 'src', + ]; + + if (!isset($Element['name'])) { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if (!empty($Element['attributes'])) { + foreach ($Element['attributes'] as $att => $val) { + # filter out badly parsed attribute + if (!preg_match($goodAttribute, $att)) { + unset($Element['attributes'][$att]); + } # dump onevent attribute + elseif (self::striAtStart($att, 'on')) { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + # + # Setext + + /** + * Filters out URLs in an element's attribute that match the unsafe URL schemes. + * + * @param array $Element The element object to be filtered + * @param string $attribute The name of the attribute to check + * + * @return array The element with any unsafely-located links replaced or removed + */ + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) { + foreach ($this->safeLinksWhitelist as $scheme) { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Markup + + /** + * Checks if the start of a string matches a given substring. + * + * This function performs a case-insensitive check to see if the provided string starts with the specified needle. + * + * @param string $string The input string to be checked + * @param string $needle The substring that must match at the start of the input string + * + * @return bool True if the string starts with the given needle, false otherwise + */ + protected static function striAtStart($string, $needle) { + $len = strlen($needle); + + if ($len > strlen($string)) { + return false; + } else { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + /** + * Handles the given element by executing its handler function if it exists. + * + * @param array $Element The element to handle, which may contain a 'handler' key + * + * @return array The handled element with any changes made by the handler function + */ + protected function handle(array $Element) { + if (isset($Element['handler'])) { + if (!isset($Element['nonNestables'])) { + $Element['nonNestables'] = []; + } + + if (is_string($Element['handler'])) { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } else { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + # + # Reference + + /** + * Escapes a string of text to prevent XSS attacks. + * + * @param string $text The input text to escape + * @param bool $allowQuotes Allow or disallow escaping of quotes (default: false) + * + * @return string The escaped text + */ + protected static function escape($text, $allowQuotes = false) { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + # + # Table + + /** + * Processes an array of lines to extract elements. + * + * @param array $lines An array of lines to process + * + * @return array An array of extracted elements + */ + protected function linesElements(array $lines) { + $Elements = []; + $CurrentBlock = null; + + foreach ($lines as $line) { + if (chop($line) === '') { + if (isset($CurrentBlock)) { + $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1); + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = ['body' => $line, 'indent' => $indent, 'text' => $text]; + + # ~ + + if (isset($CurrentBlock['continuable'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) { + $CurrentBlock = $Block; + + continue; + } else { + if ($this->isBlockCompletable($CurrentBlock['type'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) { + foreach ($this->BlockTypes[$marker] as $blockType) { + $blockTypes [] = $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) { + $Block['type'] = $blockType; + + if (!isset($Block['identified'])) { + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) { + $CurrentBlock = $Block; + } else { + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + /** + * Checks if a block can be completed with the given type. + * + * @param string $Type The type of block to check + * + * @return bool True if the block can be completed with the given type, false otherwise + */ + protected function isBlockCompletable($Type) { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # ~ + # + + /** + * Extracts the element from a component array. + * + * If the 'element' key is missing, it will be created based on the presence of other keys: + * - If the 'markup' key is present, it will be used to create an element with 'rawHtml'. + * - If the 'hidden' key is present, it will create an empty element. + * + * @param array $Component The component array containing information about a single element + * + * @return array|null The extracted element from the component array, or null if no element was found + */ + protected function extractElement(array $Component) { + if (!isset($Component['element'])) { + if (isset($Component['markup'])) { + $Component['element'] = ['rawHtml' => $Component['markup']]; + } elseif (isset($Component['hidden'])) { + $Component['element'] = []; + } + } + + return $Component['element']; + } + + /** + * Determines whether a block can be continued based on its type. + * + * @param string $Type The type of the block + * + * @return bool True if the block can be continued, false otherwise + */ + protected function isBlockContinuable($Type) { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + # + # Inline Elements + # + + /** + * Continues a paragraph by appending the given line of text to the block. + * + * If the block is interrupted, this method does nothing and returns immediately. + * + * @param array $Line A single line of text in the paragraph + * @param array $Block The block being processed, with an 'interrupted' key indicating whether it has been + * interrupted + * + * @return array The updated block with the new text appended to its element handler argument + */ + protected function paragraphContinue($Line, array $Block) { + if (isset($Block['interrupted'])) { + return; + } + + $Block['element']['handler']['argument'] .= "\n" . $Line['text']; + + return $Block; + } + + # ~ + + /** + * Creates a paragraph element in the document structure. + * + * @param array $Line An array containing text to be inserted into the paragraph + * + * @return array An array representing the paragraph element, including its type and content + */ + protected function paragraph($Line) { + return [ + 'type' => 'Paragraph', + 'element' => [ + 'name' => 'p', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ], + ], + ]; + } + + # + # ~ + # + + /** + * Processes a block of code and returns an array representing the block if it meets certain conditions. + * + * @param array $Line A line of text representing a single line in the block + * @param array|null $Block An optional block of text, expected to be null or an array with 'type' and optionally + * 'interrupted' keys + * + * @return array|void The processed block if it meets certain conditions, otherwise void is returned + */ + protected function blockCode($Line, $Block = null) { + if (isset($Block) and $Block['type'] === 'Paragraph' and !isset($Block['interrupted'])) { + return; + } + + if ($Line['indent'] >= 4) { + $text = substr($Line['body'], 4); + + $Block = [ + 'element' => [ + 'name' => 'pre', + 'element' => [ + 'name' => 'code', + 'text' => $text, + ], + ], + ]; + + return $Block; + } + } + + /** + * Continues a block of code. + * + * @param array $Line The current line of the code block + * @param array $Block The current block of the code being parsed + * + * @return array The modified block with continued code, or the original block if continuation is not applicable + */ + protected function blockCodeContinue($Line, $Block) { + if ($Line['indent'] >= 4) { + if (isset($Block['interrupted'])) { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + # + # ~ + # + + /** + * Generates a list of suggestions for the given block of code. + * + * @param string $Block The block of code to generate suggestions for + * + * @return string The original block of code, unchanged + */ + protected function blockCodeComplete($Block) { + return $Block; + } + + /** + * Parses a line of text as a block comment. + * + * @param array $Line The parsed input line, with properties 'text' and 'body'. + * + * @return array|null A Block object containing the parsed comment, or null if the input is not a valid comment + */ + protected function blockComment($Line) { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (strpos($Line['text'], '') !== false) { + $Block['closed'] = true; + } + + return $Block; + } + } + + /** + * Continues a block comment. + * + * @param array $Line The current line of code + * @param array $Block The block comment being processed + * + * @return array The updated block comment data + */ + protected function blockCommentContinue($Line, array $Block) { + if (isset($Block['closed'])) { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) { + $Block['closed'] = true; + } + + return $Block; + } + + /** + * Parses a line of text to identify fenced code blocks. + * + * @param array $Line The input data containing the line of text + * and associated information, e.g. [ 'text' => 'text', ... ] + * + * @return array|void A block object containing parsed information, + * or void if no valid fenced code block is found + */ + protected function blockFencedCode($Line) { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) { + return; + } + + $Element = [ + 'name' => 'code', + 'text' => '', + ]; + + if ($infostring !== '') { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = ['class' => "language-$language"]; + } + + $Block = [ + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => [ + 'name' => 'pre', + 'element' => $Element, + ], + ]; + + return $Block; + } + + /** + * Continues processing a fenced code block in the source input. + * + * @param array $Line The current line being processed + * @param array $Block The state of the currently open code block + * + * @return array The updated state of the code block + */ + protected function blockFencedCodeContinue($Line, $Block) { + if (isset($Block['complete'])) { + return; + } + + if (isset($Block['interrupted'])) { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + /** + * Completes block fenced code. + * + * This method completes a block fenced code by returning the provided block of code. + * + * @param mixed $Block The block of code to complete + * + * @return mixed The completed block of code + */ + protected function blockFencedCodeComplete($Block) { + return $Block; + } + + /** + * Creates a block header element. + * + * @param array $Line A line of text to parse for the block header + * + * @return array The created block header element or null if it does not meet the criteria + */ + protected function blockHeader($Line) { + $level = strspn($Line['text'], '#'); + + if ($level > 6) { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') { + return; + } + + $text = trim($text, ' '); + + $Block = [ + 'element' => [ + 'name' => 'h' . $level, + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ], + ], + ]; + + return $Block; + } + + /** + * Continues the block list processing from the previous line. + * + * This method checks if the current line should be added to the block list, + * considering the required indentation and the type of the block list. + * + * @param array $Line The current line being processed + * @param array $Block The block list data structure + * + * @return array|null The updated block list data structure, or null if a block reference is encountered + */ + protected function blockListContinue($Line, array $Block) { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++' . $Block['data']['markerTypeRegex'] . '(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^' . $Block['data']['markerTypeRegex'] . '(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) { + $Block['li']['handler']['argument'] [] = ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = [ + 'name' => 'li', + 'handler' => [ + 'function' => 'li', + 'argument' => [$text], + 'destination' => 'elements', + ], + ]; + + $Block['element']['elements'] [] = &$Block['li']; + + return $Block; + } elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) { + if (isset($Block['interrupted'])) { + $Block['li']['handler']['argument'] [] = ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] [] = $text; + + return $Block; + } + + if (!isset($Block['interrupted'])) { + $text = preg_replace('/^[ ]{0,' . $requiredIndent . '}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] [] = $text; + + return $Block; + } + } + + /** + * Processes a line of text as part of a block list (ordered or unordered). + * + * @param array $Line The input line, containing information about the indentation and text. + * @param array $CurrentBlock The current block being processed, used for context. + * + * @return array|null A block object representing the processed list, or null if not a valid list. + */ + protected function blockList($Line, array $CurrentBlock = null) { + [$name, $pattern] = $Line['text'][0] <= '-' ? ['ul', '[*+-]'] : ['ol', '[0-9]{1,9}+[.\)]']; + + if (preg_match('/^(' . $pattern . '([ ]++|$))(.*+)/', $Line['text'], $matches)) { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } elseif ($contentIndent === 0) { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = [ + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => [ + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ], + 'element' => [ + 'name' => $name, + 'elements' => [], + ], + ]; + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and !isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = ['start' => $listStart]; + } + } + + $Block['li'] = [ + 'name' => 'li', + 'handler' => [ + 'function' => 'li', + 'argument' => !empty($matches[3]) ? [$matches[3]] : [], + 'destination' => 'elements', + ], + ]; + + $Block['element']['elements'] [] = &$Block['li']; + + return $Block; + } + } + + /** + * Parses the given line to extract a block reference. + * + * @param array $Line The line to parse, containing the text of the block reference. + * + * @return array An array representing the block reference, with its element and data. + */ + protected function blockReference($Line) { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = [ + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ]; + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = [ + 'element' => [], + ]; + + return $Block; + } + } + + /** + * Completes the block list by adding a trailing argument to elements that have one. + * + * @param array $Block The block object to be completed + * + * @return array The modified block object with loose elements completed + */ + protected function blockListComplete(array $Block) { + if (isset($Block['loose'])) { + foreach ($Block['element']['elements'] as &$li) { + if (end($li['handler']['argument']) !== '') { + $li['handler']['argument'] [] = ''; + } + } + } + + return $Block; + } + + /** + * Parses a line of text and returns blockquote data if the line starts with '>' followed by one or more spaces. + * + * @param array $Line The input line to be parsed + * + * @return array|null The blockquote element data, or null if no blockquote found in the line + */ + protected function blockQuote($Line) { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { + $Block = [ + 'element' => [ + 'name' => 'blockquote', + 'handler' => [ + 'function' => 'linesElements', + 'argument' => (array)$matches[1], + 'destination' => 'elements', + ], + ], + ]; + + return $Block; + } + } + + # ~ + + /** + * Continues a block quote by adding the given line to it. + * + * @param array $Line The current line being processed + * @param array $Block The block of text that is currently being parsed + * + * @return array The updated block of text + */ + protected function blockQuoteContinue($Line, array $Block) { + if (isset($Block['interrupted'])) { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { + $Block['element']['handler']['argument'] [] = $matches[1]; + + return $Block; + } + + if (!isset($Block['interrupted'])) { + $Block['element']['handler']['argument'] [] = $Line['text']; + + return $Block; + } + } + + # + # Handlers + # + + /** + * Determines if the given line meets the block rule criteria. + * + * @param array $Line An associative array containing information about the current line + * + * @return array|null The block configuration if the line meets the block rule, otherwise null + */ + protected function blockRule($Line) { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') { + $Block = [ + 'element' => [ + 'name' => 'hr', + ], + ]; + + return $Block; + } + } + + /** + * Sets the header of a Setext block. + * + * @param array $Line The current line being processed + * @param array $Block The current block being processed (optional) + * + * @return array|null The updated block, or null if not applicable + */ + protected function blockSetextHeader($Line, array $Block = null) { + if (!isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + /** + * Processes a line of markup and returns the block if it matches certain conditions. + * + * @param array $Line The line to process, containing 'text' key + * + * @return array|null The processed block data or null if no match is found + */ + protected function blockMarkup($Line) { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+' . $this->regexHtmlAttribute . ')*+[ ]*+(\/)?>/', $Line['text'], $matches)) { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) { + return; + } + + $Block = [ + 'name' => $matches[1], + 'element' => [ + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ], + ]; + + return $Block; + } + } + + /** + * Continues the block markup by appending the provided line to the block's element raw HTML. + * + * @param array $Line The line of text containing the body content + * @param array $Block The block object with its properties and state + * + * @return array The updated block object with the appended line + */ + protected function blockMarkupContinue($Line, array $Block) { + if (isset($Block['closed']) or isset($Block['interrupted'])) { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + /** + * Generates a table block from the given line and paragraph block. + * This method creates a table based on the header and divider provided in the + * paragraph block, and returns the modified block with the new table structure. + * + * @param array $Line The current line being processed. + * @param array|null $Block The paragraph block to be processed. If null, + * the method will return immediately without modifying any data. + * + * @return array|null The modified paragraph block with a table structure, or + * null if the table cannot be generated (e.g., due to invalid header + * or divider).*/ + protected function blockTable($Line, array $Block = null) { + if (!isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') { + return; + } + + $alignments = []; + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') { + $alignment = 'left'; + } + + if (substr($dividerCell, -1) === ':') { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments [] = $alignment; + } + + # ~ + + $HeaderElements = []; + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) { + return; + } + + foreach ($headerCells as $index => $headerCell) { + $headerCell = trim($headerCell); + + $HeaderElement = [ + 'name' => 'th', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ], + ]; + + if (isset($alignments[$index])) { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = [ + 'style' => "text-align: $alignment;", + ]; + } + + $HeaderElements [] = $HeaderElement; + } + + # ~ + + $Block = [ + 'alignments' => $alignments, + 'identified' => true, + 'element' => [ + 'name' => 'table', + 'elements' => [], + ], + ]; + + $Block['element']['elements'] [] = [ + 'name' => 'thead', + ]; + + $Block['element']['elements'] [] = [ + 'name' => 'tbody', + 'elements' => [], + ]; + + $Block['element']['elements'][0]['elements'] [] = [ + 'name' => 'tr', + 'elements' => $HeaderElements, + ]; + + return $Block; + } + + /** + * Continues the block table processing. + * + * This method is called when a new line of text is encountered that continues + * a previously started block table. If the block has not been interrupted, + * it will be processed and added to the elements array. + * + * @param Line $Line The current line being processed. + * @param array $Block The current block being built up. + * + * @return array The updated Block instance if processing is successful, or + * the original Block instance (no change) if an interruption occurs. + */ + protected function blockTableContinue($Line, array $Block) { + if (isset($Block['interrupted'])) { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) { + $Elements = []; + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) { + $cell = trim($cell); + + $Element = [ + 'name' => 'td', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ], + ]; + + if (isset($Block['alignments'][$index])) { + $Element['attributes'] = [ + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ]; + } + + $Elements [] = $Element; + } + + $Element = [ + 'name' => 'tr', + 'elements' => $Elements, + ]; + + $Block['element']['elements'][1]['elements'] [] = $Element; + + return $Block; + } + } + + /** + * Processes an excerpt and returns its inline code representation. + * + * @param array $Excerpt The excerpt to process + * + * @return array An array containing the extent of the inline code and its element details + */ + protected function inlineCode($Excerpt) { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^([' . $marker . ']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), + 'element' => [ + 'name' => 'code', + 'text' => $text, + ], + ]; + } + } + + /** + * Inline email tag in the given excerpt. + * + * @param array $Excerpt The excerpt to extract inline email tags from + * + * @return array|null|null An object representing the extracted email tag, or null if no match was found + */ + protected function inlineEmailTag($Excerpt) { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ) { + $url = $matches[1]; + + if (!isset($matches[2])) { + $url = "mailto:$url"; + } + + return [ + 'extent' => strlen($matches[0]), + 'element' => [ + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => [ + 'href' => $url, + ], + ], + ]; + } + } + + /** + * Inlines emphasis text, adding it as an element to the document. + * + * @param array $Excerpt The excerpt containing the text to inline + * + * @return array|null An array describing the newly added element and its extent, + * or null if inlining was unsuccessful + */ + protected function inlineEmphasis($Excerpt) { + if (!isset($Excerpt['text'][1])) { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) { + $emphasis = 'strong'; + } elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) { + $emphasis = 'em'; + } else { + return; + } + + return [ + 'extent' => strlen($matches[0]), + 'element' => [ + 'name' => $emphasis, + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ], + ], + ]; + } + + # ~ + + /** + * Escapes a special inline escape sequence. + * + * @param array $Excerpt The excerpt to extract the escape sequence from + * + * @return array|void An object representing the escaped element, or null if no escape was found + */ + protected function inlineEscapeSequence($Excerpt) { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) { + return [ + 'element' => ['rawHtml' => $Excerpt['text'][1]], + 'extent' => 2, + ]; + } + } + + # + # AST Convenience + # + + /** + * Inserts an inline image element into the parsed excerpt. + * + * @param array $Excerpt The parsed excerpt containing text and other data + * + * @return array|null An array representing the inserted inline image, or null if the excerpt does not contain a + * valid link to insert as an image + */ + protected function inlineImage($Excerpt) { + if (!isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') { + return; + } + + $Excerpt['text'] = substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) { + return; + } + + $Inline = [ + 'extent' => $Link['extent'] + 1, + 'element' => [ + 'name' => 'img', + 'attributes' => [ + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ], + 'autobreak' => true, + ], + ]; + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + # + # Deprecated Methods + # + + /** + * Processes an excerpt and extracts inline links. + * + * @param array $Excerpt An excerpt object containing text data + * + * @return array|null A result object containing the extent of processing and the element, or null if no link was + * found + */ + protected function inlineLink($Excerpt) { + $Element = [ + 'name' => 'a', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ], + 'nonNestables' => ['Url', 'Link'], + 'attributes' => [ + 'href' => null, + 'title' => null, + ], + ]; + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } else { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) { + $Element['attributes']['title'] = substr($matches[2], 1, -1); + } + + $extent += strlen($matches[0]); + } else { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } else { + $definition = strtolower($Element['handler']['argument']); + } + + if (!isset($this->DefinitionData['Reference'][$definition])) { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return [ + 'extent' => $extent, + 'element' => $Element, + ]; + } + + protected function inlineMarkup($Excerpt) { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) { + return [ + 'element' => ['rawHtml' => $matches[0]], + 'extent' => strlen($matches[0]), + ]; + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) { + return [ + 'element' => ['rawHtml' => $matches[0]], + 'extent' => strlen($matches[0]), + ]; + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+' . $this->regexHtmlAttribute . ')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) { + return [ + 'element' => ['rawHtml' => $matches[0]], + 'extent' => strlen($matches[0]), + ]; + } + } + + /** + * Inlines a special character within the provided excerpt. + * + * @param array $Excerpt The excerpt object containing 'text' + * + * @return array|null An array containing an inline element and extent, or null if no match + */ + protected function inlineSpecialCharacter($Excerpt) { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return [ + 'element' => ['rawHtml' => '&' . $matches[1] . ';'], + 'extent' => strlen($matches[0]), + ]; + } + + return; + } + + # + # Static Methods + # + + /** + * Applies inline strikethrough formatting to the provided excerpt. + * + * @param array $Excerpt The excerpt containing text that may need strikethrough formatting + * + * @return array|null An object with 'extent' and 'element' properties if strikethrough formatting is applied, + * otherwise null + */ + protected function inlineStrikethrough($Excerpt) { + if (!isset($Excerpt['text'][1])) { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) { + return [ + 'extent' => strlen($matches[0]), + 'element' => [ + 'name' => 'del', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ], + ], + ]; + } + } + + /** + * Inlines a URL within the given excerpt. + * + * If an array of linked URLs is set, and the excerpt's text is not empty at index 2 (i.e. it starts with + * 'http://') or the context already contains a full URL, this method creates an inline element (an anchor tag) + * that links to the extracted URL. + * + * @param array $Excerpt The excerpt containing the text and context + * + * @return array|null An array representing the inlined element, or null if no link is created + */ + protected function inlineUrl($Excerpt) { + if ($this->urlsLinked !== true or !isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = [ + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => [ + 'name' => 'a', + 'text' => $url, + 'attributes' => [ + 'href' => $url, + ], + ], + ]; + + return $Inline; + } + } + + /** + * Inserts an inline URL tag into the excerpt. + * + * @param array $Excerpt The excerpt object + * + * @return array|null An array containing the extent and element details if a valid URL is found, otherwise null + */ + protected function inlineUrlTag($Excerpt) { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) { + $url = $matches[1]; + + return [ + 'extent' => strlen($matches[0]), + 'element' => [ + 'name' => 'a', + 'text' => $url, + 'attributes' => [ + 'href' => $url, + ], + ], + ]; + } + } + + /** + * Returns an element containing inline text content. + * + * @param string $text The text to be displayed as inline content + * + * @return mixed An element representing the unmarked text content + */ + protected function unmarkedText($text) { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Fields + # + + /** + * Recursively handles an element using the 'handle' method. + * + * @param array $Element The element to be handled recursively + * + * @return mixed The result of handling the element recursively + */ + protected function handleElementRecursive(array $Element) { + return $this->elementApplyRecursive([$this, 'handle'], $Element); + } + + # + # Read-Only + + /** + * Recursively applies a closure to an element. + * + * @param callable $closure A function that will be applied to the element + * @param array $Element The element to which the closure should be applied + * + * @return array The modified element with any child elements also updated + */ + protected function elementApplyRecursive($closure, array $Element) { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } elseif (isset($Element['element'])) { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + /** + * Recursively applies a closure to each element in the given collection of elements. + * + * @param callable $closure The function to apply to each element + * @param array $Elements A collection of elements to apply the closure to + * + * @return array The modified collection of elements with the closure applied + */ + protected function elementsApplyRecursive($closure, array $Elements) { + foreach ($Elements as &$Element) { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + /** + * Recursively applies the element handling logic to the given elements. + * + * @param array $Elements The elements to process + * + * @return self The instance of the class for chaining method calls + */ + protected function handleElementsRecursive(array $Elements) { + return $this->elementsApplyRecursive([$this, 'handle'], $Elements); + } + + /** + * Applies a closure to an element recursively in depth-first order. + * + * @param callable $closure A function that will be applied to the element + * @param array $element An array containing the element and its properties + * + * @return array The modified element after applying the closure + */ + protected function elementApplyRecursiveDepthFirst($closure, array $Element) { + if (isset($Element['elements'])) { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } elseif (isset($Element['element'])) { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + /** + * Applies a closure recursively to an array of elements using depth-first order. + * + * @param callable $closure A function that will be applied to each element + * @param array $Elements An array of elements to apply the closure to + * + * @return array The modified array of elements with the closure applied + */ + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) { + foreach ($Elements as &$Element) { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + /** + * Process an array of lines to determine the HTML elements contained within. + * + * @param array $lines An array of lines that may contain HTML elements + * + * @return array An array containing parsed HTML element data for each line + */ + protected function li($lines) { + $Elements = $this->linesElements($lines); + + if (!in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } } diff --git a/resources/classes/permissions.php b/resources/classes/permissions.php index 651f42c1c7..8dadc38c7c 100644 --- a/resources/classes/permissions.php +++ b/resources/classes/permissions.php @@ -26,15 +26,23 @@ */ class permissions { + private static $permission; private $database; private $domain_uuid; private $user_uuid; private $groups; private $permissions; - private static $permission; /** - * called when the object is created + * Constructor. + * + * Initializes this object with a database connection, domain UUID, and user UUID. + * + * @param Database|null $database Database connection. If null, a new database connection will be created. + * @param string|null $domain_uuid Domain UUID. If null, the value from the session will be used. + * @param string|null $user_uuid User UUID. If null, the value from the session will be used. + * + * @return void */ public function __construct($database = null, $domain_uuid = null, $user_uuid = null) { @@ -45,32 +53,28 @@ class permissions { //handle the database object if (isset($database)) { $this->database = $database; - } - else { + } else { $this->database = database::new(); } //set the domain_uuid if (!empty($domain_uuid) && is_uuid($domain_uuid)) { $this->domain_uuid = $domain_uuid; - } - elseif (isset($_SESSION['domain_uuid']) && is_uuid($_SESSION['domain_uuid'])) { + } elseif (isset($_SESSION['domain_uuid']) && is_uuid($_SESSION['domain_uuid'])) { $this->domain_uuid = $_SESSION['domain_uuid']; } //set the user_uuid if (!empty($user_uuid) && is_uuid($user_uuid)) { $this->user_uuid = $user_uuid; - } - elseif (isset($_SESSION['user_uuid']) && is_uuid($_SESSION['user_uuid'])) { + } elseif (isset($_SESSION['user_uuid']) && is_uuid($_SESSION['user_uuid'])) { $this->user_uuid = $_SESSION['user_uuid']; } //get the permissions if (isset($_SESSION['permissions'])) { $this->permissions = $_SESSION['permissions']; - } - else { + } else { //create the groups object $groups = new groups($this->database, $this->domain_uuid, $this->user_uuid); $this->groups = $groups->assigned(); @@ -83,15 +87,92 @@ class permissions { } /** - * get the array of permissions + * A singleton pattern for either creating a new object or the existing object. + * + * Initializes this object with a database connection, domain UUID, and user UUID. + * + * @param Database|null $database Database connection. If null, a new database connection will be created. + * @param string|null $domain_uuid Domain UUID. If null, the value from the session will be used. + * @param string|null $user_uuid User UUID. If null, the value from the session will be used. + * + * @return self + */ + public static function new($database = null, $domain_uuid = null, $user_uuid = null) { + if (self::$permission === null) { + self::$permission = new permissions($database, $domain_uuid, $user_uuid); + } + return self::$permission; + } + + /** + * Method to retrieve permissions assigned to the user through their groups. + * + * Retrieves the list of group names associated with the user's assigned groups, + * and then uses these group names to query for distinct permission names that are + * assigned to these groups. The resulting list of permission names is stored in + * this object's 'permissions' array. + * + * @return void + */ + private function assigned() { + //define the array + $permissions = []; + $parameter_names = []; + + //return empty array if there are no groups + if (empty($this->groups)) { + return []; + } + + //prepare the parameters + $x = 0; + foreach ($this->groups as $field) { + if (!empty($field['group_name'])) { + $parameter_names[] = ":group_name_" . $x; + $parameters['group_name_' . $x] = $field['group_name']; + $x++; + } + } + + //get the permissions assigned to the user through the assigned groups + $sql = "select distinct(permission_name) from v_group_permissions "; + $sql .= "where (domain_uuid = :domain_uuid or domain_uuid is null) "; + $sql .= "and group_name in (" . implode(", ", $parameter_names) . ") \n"; + $sql .= "and permission_assigned = 'true' "; + $parameters['domain_uuid'] = $this->domain_uuid; + $group_permissions = $this->database->select($sql, $parameters, 'all'); + + //format the permission array + foreach ($group_permissions as $row) { + $permissions[$row['permission_name']] = 1; + } + + //save permissions to this object + $this->permissions = $permissions; + } + + /** + * Returns an array of permissions assigned to this user. + * + * The list of permissions is populated from the session or retrieved from the database based on + * the domain UUID and user UUID associated with this object. + * + * @return array An array of permission identifiers (e.g. 'create_user', 'edit_group', etc.) */ public function get_permissions() { return $this->permissions; } /** - * Add the permission - * @var string $permission + * Adds a permission to this object. + * + * If the specified permission does not already exist, it will be added to the permissions array with the provided + * type. + * + * @param string $permission Permission to add. + * @param mixed $type Type of the permission. + * + * @return void */ public function add($permission, $type) { //add the permission if it is not in array @@ -101,27 +182,13 @@ class permissions { } /** - * Remove the permission - * @var string $permission - */ - public function delete($permission, $type) { - if ($this->exists($permission) && !empty($this->permissions[$permission])) { - if ($type === "temp") { - if ($this->permissions[$permission] === "temp") { - unset($this->permissions[$permission]); - } - } - else { - if ($this->permissions[$permission] !== "temp") { - unset($this->permissions[$permission]); - } - } - } - } - - /** - * Check to see if the permission exists - * @var string $permission + * Checks if a permission exists. + * + * Returns true if the permission is assigned to the user, or if this method is called from the command line. + * + * @param string $permission_name Name of the permission to check for existence. + * + * @return bool True if the permission exists, false otherwise. */ public function exists($permission_name) { @@ -139,48 +206,33 @@ class permissions { } /** - * get the assigned permissions - * @var array $groups + * Deletes a permission. + * + * If the permission exists and is not temporary, it will be removed from the permissions array. + * + * @param string $permission The name of the permission to delete. + * @param string $type The type of permission (e.g. "temp", "permanent"). + * + * @return void */ - private function assigned() { - //define the array - $permissions = []; - $parameter_names = []; - - //return empty array if there are no groups - if (empty($this->groups)) { - return []; - } - - //prepare the parameters - $x = 0; - foreach ($this->groups as $field) { - if (!empty($field['group_name'])) { - $parameter_names[] = ":group_name_".$x; - $parameters['group_name_'.$x] = $field['group_name']; - $x++; + public function delete($permission, $type) { + if ($this->exists($permission) && !empty($this->permissions[$permission])) { + if ($type === "temp") { + if ($this->permissions[$permission] === "temp") { + unset($this->permissions[$permission]); + } + } else { + if ($this->permissions[$permission] !== "temp") { + unset($this->permissions[$permission]); + } } } - - //get the permissions assigned to the user through the assigned groups - $sql = "select distinct(permission_name) from v_group_permissions "; - $sql .= "where (domain_uuid = :domain_uuid or domain_uuid is null) "; - $sql .= "and group_name in (".implode(", ", $parameter_names).") \n"; - $sql .= "and permission_assigned = 'true' "; - $parameters['domain_uuid'] = $this->domain_uuid; - $group_permissions = $this->database->select($sql, $parameters, 'all'); - - //format the permission array - foreach ($group_permissions as $row) { - $permissions[$row['permission_name']] = 1; - } - - //save permissions to this object - $this->permissions = $permissions; } /** - * save the assigned permissions to a session + * Saves the current permissions to the session. + * + * @return void */ public function session() { if (!empty($this->permissions)) { @@ -191,24 +243,14 @@ class permissions { } } - /** - * Returns a new permission object - */ - public static function new($database = null, $domain_uuid = null, $user_uuid = null) { - if (self::$permission === null) { - self::$permission = new permissions($database, $domain_uuid, $user_uuid); - } - return self::$permission; - } - } //examples - /* - //add the permission - $p = permissions::new(); - $p->add($permission); - //delete the permission - $p = permissions::new(); - $p->delete($permission); - */ +/* +//add the permission + $p = permissions::new(); + $p->add($permission); +//delete the permission + $p = permissions::new(); + $p->delete($permission); +*/ diff --git a/resources/classes/schema.php b/resources/classes/schema.php index 115816c597..de33274e66 100644 --- a/resources/classes/schema.php +++ b/resources/classes/schema.php @@ -26,854 +26,986 @@ */ //define the schema class - class schema { +class schema { - //define the public variables - public $data_types; - public $result; + //define the public variables + public $data_types; + public $result; - //define private variables - private $database; - private $applications; - private $db_type; - private $db_name; - private $schema_info; + //define private variables + private $database; + private $applications; + private $db_type; + private $db_name; + private $schema_info; - /** - * called when the object is created - */ - public function __construct($setting_array = []) { + /** + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $setting_array An optional array of settings to override default values. Defaults to []. + */ + public function __construct($setting_array = []) { - //set the global variables - global $db_type, $db_name; + //set the global variables + global $db_type, $db_name; - //includes files - require dirname(__DIR__, 2) . "/resources/require.php"; + //includes files + require dirname(__DIR__, 2) . "/resources/require.php"; - //open a database connection - $this->database = $setting_array['database'] ?? database::new(); + //open a database connection + $this->database = $setting_array['database'] ?? database::new(); - //get the list of installed apps from the core and mod directories - $config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php"); - $x = 0; - foreach ($config_list as $config_path) { - try { - include($config_path); - } catch (Exception $e) { - //echo 'Caught exception: ', $e->getMessage(), "\n"; - } - $x++; + //get the list of installed apps from the core and mod directories + $config_list = glob($_SERVER["DOCUMENT_ROOT"] . PROJECT_PATH . "/*/*/app_config.php"); + $x = 0; + foreach ($config_list as $config_path) { + try { + include($config_path); + } catch (Exception $e) { + //echo 'Caught exception: ', $e->getMessage(), "\n"; } - $this->applications = $apps; + $x++; + } + $this->applications = $apps; - //set the db_type - $this->db_type = $db_type; + //set the db_type + $this->db_type = $db_type; - //set the db_name - $this->db_name = $db_name; + //set the db_name + $this->db_name = $db_name; - //get the table info - if ($this->db_type == "pgsql") { - $sql = "SELECT *, ordinal_position, "; - $sql .= "table_name, "; - $sql .= "column_name, "; - $sql .= "data_type, "; - $sql .= "column_default, "; - $sql .= "is_nullable, "; - $sql .= "character_maximum_length, "; - $sql .= "numeric_precision "; - $sql .= "FROM information_schema.columns "; - $sql .= "WHERE table_catalog = '" . $db_name . "' "; - $sql .= "and table_schema not in ('pg_catalog', 'information_schema') "; - $sql .= "ORDER BY ordinal_position; "; - $schema = $this->database->select($sql, null, 'all'); - foreach($schema as $row) { - $this->schema_info[$row['table_name']][] = $row; - } + //get the table info + if ($this->db_type == "pgsql") { + $sql = "SELECT *, ordinal_position, "; + $sql .= "table_name, "; + $sql .= "column_name, "; + $sql .= "data_type, "; + $sql .= "column_default, "; + $sql .= "is_nullable, "; + $sql .= "character_maximum_length, "; + $sql .= "numeric_precision "; + $sql .= "FROM information_schema.columns "; + $sql .= "WHERE table_catalog = '" . $db_name . "' "; + $sql .= "and table_schema not in ('pg_catalog', 'information_schema') "; + $sql .= "ORDER BY ordinal_position; "; + $schema = $this->database->select($sql, null, 'all'); + foreach ($schema as $row) { + $this->schema_info[$row['table_name']][] = $row; } } + } - //create the database schema - public function sql() { - $sql = ''; - $sql_schema = ''; - foreach ($this->applications as $app) { - if (isset($app['db']) && count($app['db'])) { - foreach ($app['db'] as $row) { - //create the sql string - $table_name = $row['table']['name']; - $sql = "CREATE TABLE " . $row['table']['name'] . " (\n"; - $field_count = 0; - foreach ($row['fields'] as $field) { - if (!empty($field['deprecated']) and ($field['deprecated'] == "true")) { - //skip this field - } else { - if ($field_count > 0) { - $sql .= ",\n"; - } - if (is_array($field['name'])) { - $sql .= $field['name']['text'] . " "; - } else { - $sql .= $field['name'] . " "; - } - if (is_array($field['type'])) { - $sql .= $field['type'][$this->db_type]; - } else { - $sql .= $field['type']; - } - if (isset($field['key']) && isset($field['key']['type']) && ($field['key']['type'] == "primary")) { - $sql .= " PRIMARY KEY"; - } - if (isset($field['key']) && isset($field['key']['type']) && ($field['key']['type'] == "foreign")) { - if ($this->db_type == "pgsql") { - //$sql .= " references ".$field['key']['reference']['table']."(".$field['key']['reference']['field'].")"; - } - if ($this->db_type == "sqlite") { - //$sql .= " references ".$field['key']['reference']['table']."(".$field['key']['reference']['field'].")"; - } - if ($this->db_type == "mysql") { - //$sql .= " references ".$field['key']['reference']['table']."(".$field['key']['reference']['field'].")"; - } - } - $field_count++; - } - } - if ($this->db_type == "mysql") { - $sql .= ") ENGINE=INNODB;"; + //create the database schema + + /** + * Generate SQL statements for creating tables. + * + * This method loops through the list of applications and generates CREATE TABLE + * SQL statements based on the table definitions provided in each application's database settings. + * + * @return array An array containing the generated SQL statements. + */ + public function sql() { + $sql = ''; + $sql_schema = ''; + foreach ($this->applications as $app) { + if (isset($app['db']) && count($app['db'])) { + foreach ($app['db'] as $row) { + //create the sql string + $table_name = $row['table']['name']; + $sql = "CREATE TABLE " . $row['table']['name'] . " (\n"; + $field_count = 0; + foreach ($row['fields'] as $field) { + if (!empty($field['deprecated']) and ($field['deprecated'] == "true")) { + //skip this field } else { - $sql .= ");"; - } - $this->result['sql'][] = $sql; - unset($sql); - } - } - } - } - - //add the database schema - public function exec() { - foreach ($this->result['sql'] as $sql) { - //start the sql transaction - $this->database->beginTransaction(); - //execute the sql query - try { - $this->database->execute($sql, null); - } catch (PDOException $error) { - echo "error: " . $error->getMessage() . " sql: $sql
    "; - } - //complete the transaction - $this->database->commit(); - } - } - - //check if a column exists - public function column_exists($table_name, $column_name) { - if (empty($table_name)) { - return false; - } - if (empty($column_name)) { - return false; - } - if ($this->db_type == "sqlite") { - $table_info = $this->table_info($table_name); - if ($this->sqlite_column_exists($table_info, $column_name)) { - return true; - } else { - return false; - } - } - if ($this->db_type == "pgsql") { - if (!isset($this->schema_info[$table_name])) { - return false; - } - foreach($this->schema_info[$table_name] as $row) { - if ($row['column_name'] == $column_name) { - return true; - } - } - return false; - } - if ($this->db_type == "mysql") { - //$sql .= "SELECT * FROM information_schema.COLUMNS where TABLE_SCHEMA = '".$this->db_name."' and TABLE_NAME = '$table_name' and COLUMN_NAME = '$column_name' "; - $sql = "show columns from $table_name where field = '$column_name' "; - } - - if ($sql) { - $prep_statement = $this->database->db->prepare($sql); - $prep_statement->execute(); - $result = $prep_statement->fetchAll(PDO::FETCH_NAMED); - unset($prep_statement); - if (!$result) { - return false; - } - if (count($result) > 0) { - return true; - } else { - return false; - } - } - } - - //get the table information - public function table_info($table_name) { - if (empty($table_name)) { - return false; - } - if ($this->db_type == "pgsql") { - if (!isset($this->schema_info[$table_name])) { - return false; - } - return $this->schema_info[$table_name]; - } - if ($this->db_type == "sqlite") { - $sql = "PRAGMA table_info(" . $table_name . ");"; - } - if ($this->db_type == "mysql") { - $sql = "describe " . $table_name . ";"; - } - $prep_statement = $this->database->db->prepare($sql); - $prep_statement->execute(); - return $prep_statement->fetchAll(PDO::FETCH_ASSOC); - } - - //database table exists - private function table_exists($table_name) { - if ($this->db_type == 'pgsql') { - if (isset($this->schema_info[$table_name])) { - return true; - } else { - return false; - } - } - if ($this->db_type == 'sqlite' || $this->db_type == 'msyql') { - $sql = "select count(*) from $table_name "; - $result = $this->database->execute($sql, null); - if ($result > 0) { - return true; //table exists - } else { - return false; //table doesn't exist - } - } - } - - //database type - private function data_type($table_info, $column_name) { - if ($this->db_type == "sqlite") { - foreach ($table_info as $key => $row) { - if ($row['name'] == $column_name) { - return $row['type']; - } - } - } - if ($this->db_type == "pgsql") { - foreach ($table_info as $key => $row) { - if ($row['column_name'] == $column_name) { - return $row['data_type']; - } - } - } - if ($this->db_type == "mysql") { - foreach ($table_info as $key => $row) { - if ($row['Field'] == $column_name) { - return $row['Type']; - } - } - } - } - - //sqlite column exists - private function sqlite_column_exists($table_info, $column_name) { - foreach ($table_info as $key => $row) { - if ($row['name'] == $column_name) { - return true; - } - } - return false; - } - - //database column data type - private function column_data_type($table_name, $column_name) { - $table_info = $this->table_info($table_name); - return $this->data_type($table_info, $column_name); - } - - //database create table - public function create_table($apps, $table) { - if (empty($apps)) { - return false; - } - if (is_array($apps)) { - foreach ($apps as $x => $app) { - if (!empty($app['db']) && is_array($app['db'])) { - foreach ($app['db'] as $y => $row) { - if (!empty($row['table']['name']) && is_array($row['table']['name'])) { - $table_name = $row['table']['name']['text']; + if ($field_count > 0) { + $sql .= ",\n"; + } + if (is_array($field['name'])) { + $sql .= $field['name']['text'] . " "; } else { - $table_name = $row['table']['name']; + $sql .= $field['name'] . " "; } - if ($table_name == $table) { - $sql = "CREATE TABLE " . $table_name . " (\n"; - (int) $field_count = 0; - if (!empty($row['fields']) && is_array($row['fields'])) { - foreach ($row['fields'] as $field) { - if (!empty($field['deprecated']) && $field['deprecated'] == "true") { - //skip this row - } else { - if ($field_count > 0) { - $sql .= ",\n"; - } - if (!empty($field['name']) && is_array($field['name'])) { - $sql .= $field['name']['text'] . " "; - } else { - $sql .= $field['name'] . " "; - } - if (!empty($field['type']) && is_array($field['type'])) { - $sql .= $field['type'][$this->db_type]; - } else { - $sql .= $field['type']; - } - if (!empty($field['key']['type']) && $field['key']['type'] == "primary") { - $sql .= " PRIMARY KEY"; - } - $field_count++; - } - } + if (is_array($field['type'])) { + $sql .= $field['type'][$this->db_type]; + } else { + $sql .= $field['type']; + } + if (isset($field['key']) && isset($field['key']['type']) && ($field['key']['type'] == "primary")) { + $sql .= " PRIMARY KEY"; + } + if (isset($field['key']) && isset($field['key']['type']) && ($field['key']['type'] == "foreign")) { + if ($this->db_type == "pgsql") { + //$sql .= " references ".$field['key']['reference']['table']."(".$field['key']['reference']['field'].")"; + } + if ($this->db_type == "sqlite") { + //$sql .= " references ".$field['key']['reference']['table']."(".$field['key']['reference']['field'].")"; + } + if ($this->db_type == "mysql") { + //$sql .= " references ".$field['key']['reference']['table']."(".$field['key']['reference']['field'].")"; } - $sql .= ");\n"; - return $sql; } + $field_count++; } } + if ($this->db_type == "mysql") { + $sql .= ") ENGINE=INNODB;"; + } else { + $sql .= ");"; + } + $this->result['sql'][] = $sql; + unset($sql); } } } + } - //database insert - private function insert_into($apps, $table) { - foreach ($apps as $x => $app) { + //add the database schema + + /** + * Executes the SQL queries in the result array. + * + * This method iterates over the SQL queries stored in the 'sql' key of the result array, + * executes each query, and commits the changes to the database. Any errors encountered + * during execution are caught and logged to the console. + * + * @return void + */ + public function exec() { + foreach ($this->result['sql'] as $sql) { + //start the sql transaction + $this->database->beginTransaction(); + //execute the sql query + try { + $this->database->execute($sql, null); + } catch (PDOException $error) { + echo "error: " . $error->getMessage() . " sql: $sql
    "; + } + //complete the transaction + $this->database->commit(); + } + } + + //check if a column exists + +/** + * Generates the schema for the provided applications. + * + * This method iterates through the application configurations and database + * definitions to determine the table and field existence. The results are stored + * in the application configurations. + * + * @param string $format The output format (default: ''). + * + * @return void + */ + public function schema($format = '') { + + //set the global variable + global $text, $output_format; + + if ($format == '') { + $format = $output_format; + } + + //get the db variables + //$config = new config; + //$config_exists = $config->exists(); + //$config_path = $config->find(); + //$config->get(); + //$this->db_type = $config->db_type; + //$this->db_name = $config->db_name; + //$db_username = $config->db_username; + //$db_password = $config->db_password; + //$db_host = $config->db_host; + //$db_path = $config->db_path; + //$db_port = $config->db_port; + //includes files + + //add multi-lingual support + if (!isset($text)) { + $language = new text; + $text = $language->get(null, 'core/upgrade'); + } + + //PHP PDO check if table or column exists + //check if table exists + // SELECT * FROM sqlite_master WHERE type='table' AND name='v_cdr' + //check if column exists + // SELECT * FROM sqlite_master WHERE type='table' AND name='v_cdr' AND sql LIKE '%caller_id_name TEXT,%' + //aditional information + // http://www.sqlite.org/faq.html#q9 + //postgresql + //list all tables in the database + // SELECT table_name FROM pg_tables WHERE schemaname='public'; + //check if table exists + // SELECT * FROM pg_tables WHERE schemaname='public' AND table_name = 'v_groups' + //check if column exists + // SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'v_cdr') AND attname = 'caller_id_name'; + //mysql + //list all tables in the database + // SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = 'fusionpbx' + //check if table exists + // SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = 'fusionpbx' AND TABLE_NAME = 'v_groups' + //check if column exists + // SELECT * FROM information_schema.COLUMNS where TABLE_SCHEMA = 'fusionpbx' AND TABLE_NAME = 'v_cdr' AND COLUMN_NAME = 'context' + //oracle + //check if table exists + // SELECT TABLE_NAME FROM ALL_TABLES + //update the app db array add exists true or false + $sql = ''; + foreach ($this->applications as $x => $app) { + if (isset($app['db'])) { foreach ($app['db'] as $y => $row) { - if ($row['table']['name'] == $table) { - $sql = "INSERT INTO " . $row['table']['name'] . " ("; - $field_count = 0; - foreach ($row['fields'] as $field) { - if (!empty($field['deprecated']) && $field['deprecated'] == "true") { - //skip this field - } else { - if ($field_count > 0) { - $sql .= ","; - } - if (is_array($field['name'])) { - $sql .= $field['name']['text']; - } else { - $sql .= $field['name']; - } - $field_count++; - } + if (isset($row['table']['name'])) { + if (is_array($row['table']['name'])) { + $table_name = $row['table']['name']['text']; + } else { + $table_name = $row['table']['name']; } - $sql .= ")\n"; - $sql .= "SELECT "; - $field_count = 0; - foreach ($row['fields'] as $field) { + } else { + //old array syntax + if (is_array($row['table'])) { + $table_name = $row['table']['text']; + } else { + $table_name = $row['table']; + } + } + if (!empty($table_name)) { + + //check if the table exists + if ($this->table_exists($table_name)) { + $this->applications[$x]['db'][$y]['exists'] = 'true'; + } else { + $this->applications[$x]['db'][$y]['exists'] = 'false'; + } + //check if the column exists + foreach ($row['fields'] as $z => $field) { if (!empty($field['deprecated']) && $field['deprecated'] == "true") { //skip this field } else { - if ($field_count > 0) { - $sql .= ","; - } if (is_array($field['name'])) { - if ($field['exists'] == "false") { - if (is_array($field['name']['deprecated'])) { - $found = false; - foreach ($field['name']['deprecated'] as $row) { - if ($this->column_exists('tmp_' . $table, $row)) { - $sql .= $row; - $found = true; - break; - } - } - if (!$found) { - $sql .= "''"; - } - } else { - if ($this->column_exists('tmp_' . $table, $field['name']['deprecated'])) { - $sql .= $field['name']['deprecated']; - } else { - $sql .= "''"; - } - } + $field_name = $field['name']['text']; + } else { + $field_name = $field['name']; + } + if (!empty($field_name)) { + if ($this->column_exists($table_name, $field_name)) { + //found + $this->applications[$x]['db'][$y]['fields'][$z]['exists'] = 'true'; } else { - $sql .= $field['name']['text']; + //not found + $this->applications[$x]['db'][$y]['fields'][$z]['exists'] = 'false'; } - } else { - $sql .= $field['name']; } - $field_count++; + unset($field_name); } } - $sql .= " FROM tmp_" . $table . ";\n"; - return $sql; + unset($table_name); } } } } - //datatase schema - public function schema($format = '') { + //prepare the variables + $sql_update = ''; - //set the global variable - global $text, $output_format; - - if ($format == '') { - $format = $output_format; - } - - //get the db variables - //$config = new config; - //$config_exists = $config->exists(); - //$config_path = $config->find(); - //$config->get(); - //$this->db_type = $config->db_type; - //$this->db_name = $config->db_name; - //$db_username = $config->db_username; - //$db_password = $config->db_password; - //$db_host = $config->db_host; - //$db_path = $config->db_path; - //$db_port = $config->db_port; - //includes files - - //add multi-lingual support - if (!isset($text)) { - $language = new text; - $text = $language->get(null, 'core/upgrade'); - } - - //PHP PDO check if table or column exists - //check if table exists - // SELECT * FROM sqlite_master WHERE type='table' AND name='v_cdr' - //check if column exists - // SELECT * FROM sqlite_master WHERE type='table' AND name='v_cdr' AND sql LIKE '%caller_id_name TEXT,%' - //aditional information - // http://www.sqlite.org/faq.html#q9 - //postgresql - //list all tables in the database - // SELECT table_name FROM pg_tables WHERE schemaname='public'; - //check if table exists - // SELECT * FROM pg_tables WHERE schemaname='public' AND table_name = 'v_groups' - //check if column exists - // SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'v_cdr') AND attname = 'caller_id_name'; - //mysql - //list all tables in the database - // SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = 'fusionpbx' - //check if table exists - // SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = 'fusionpbx' AND TABLE_NAME = 'v_groups' - //check if column exists - // SELECT * FROM information_schema.COLUMNS where TABLE_SCHEMA = 'fusionpbx' AND TABLE_NAME = 'v_cdr' AND COLUMN_NAME = 'context' - //oracle - //check if table exists - // SELECT TABLE_NAME FROM ALL_TABLES - //update the app db array add exists true or false - $sql = ''; - foreach ($this->applications as $x => $app) { - if (isset($app['db'])) { - foreach ($app['db'] as $y => $row) { - if (isset($row['table']['name'])) { - if (is_array($row['table']['name'])) { - $table_name = $row['table']['name']['text']; - } else { - $table_name = $row['table']['name']; + //add missing tables and fields + foreach ($this->applications as $x => $app) { + if (isset($app['db'])) { + foreach ($app['db'] as $y => $row) { + if (is_array($row['table']['name'])) { + $table_name = $row['table']['name']['text']; + if ($this->table_exists($row['table']['name']['deprecated'])) { + $row['exists'] = "false"; //testing + if ($this->db_type == "pgsql") { + $sql_update .= "ALTER TABLE " . $row['table']['name']['deprecated'] . " RENAME TO " . $row['table']['name']['text'] . ";\n"; + } + if ($this->db_type == "mysql") { + $sql_update .= "RENAME TABLE " . $row['table']['name']['deprecated'] . " TO " . $row['table']['name']['text'] . ";\n"; + } + if ($this->db_type == "sqlite") { + $sql_update .= "ALTER TABLE " . $row['table']['name']['deprecated'] . " RENAME TO " . $row['table']['name']['text'] . ";\n"; } } else { - //old array syntax - if (is_array($row['table'])) { - $table_name = $row['table']['text']; + if ($this->table_exists($row['table']['name']['text'])) { + $row['exists'] = "true"; } else { - $table_name = $row['table']; + $row['exists'] = "false"; + $sql_update .= $this->create_table($this->applications, $row['table']['name']['text']); } } - if (!empty($table_name)) { + } else { + if ($this->table_exists($row['table']['name'])) { + $row['exists'] = "true"; + } else { + $row['exists'] = "false"; + } + $table_name = $row['table']['name']; + } - //check if the table exists - if ($this->table_exists($table_name)) { - $this->applications[$x]['db'][$y]['exists'] = 'true'; - } else { - $this->applications[$x]['db'][$y]['exists'] = 'false'; - } - //check if the column exists + //check if the table exists + if ($row['exists'] == "true") { + if (count($row['fields']) > 0) { foreach ($row['fields'] as $z => $field) { if (!empty($field['deprecated']) && $field['deprecated'] == "true") { //skip this field } else { + //get the data type + if (is_array($field['type'])) { + $field_type = $field['type'][$this->db_type]; + } else { + $field_type = $field['type']; + } + //get the field name if (is_array($field['name'])) { $field_name = $field['name']['text']; } else { $field_name = $field['name']; } - if (!empty($field_name)) { - if ($this->column_exists($table_name, $field_name)) { - //found - $this->applications[$x]['db'][$y]['fields'][$z]['exists'] = 'true'; - } else { - //not found - $this->applications[$x]['db'][$y]['fields'][$z]['exists'] = 'false'; + + //check if the field exists + // if ($this->column_exists($table_name, $field_name)) { + // $field['exists'] = "true"; + // } + // else { + // $field['exists'] = "false"; + // } + //add or rename fields + if (isset($field['name']['deprecated']) && $this->column_exists($table_name, $field['name']['deprecated'])) { + if ($this->db_type == "pgsql") { + $sql_update .= "ALTER TABLE " . $table_name . " RENAME COLUMN " . $field['name']['deprecated'] . " to " . $field['name']['text'] . ";\n"; + } + if ($this->db_type == "mysql") { + $field_type = str_replace("AUTO_INCREMENT PRIMARY KEY", "", $field_type); + $sql_update .= "ALTER TABLE " . $table_name . " CHANGE " . $field['name']['deprecated'] . " " . $field['name']['text'] . " " . $field_type . ";\n"; + } + if ($this->db_type == "sqlite") { + //a change has been made to the field name + $this->applications[$x]['db'][$y]['rebuild'] = 'true'; + } + } else { + //find missing fields and add them + if ($field['exists'] == "false") { + $sql_update .= "ALTER TABLE " . $table_name . " ADD " . $field_name . " " . $field_type . ";\n"; + } + } + + //change the schema data types if needed + //if the data type described in the app_config array is different than the type in the database then update the data type + $db_field_type = $this->column_data_type($table_name, $field_name); + $field_type_array = explode("(", $field_type); + $field_type = $field_type_array[0]; + if (trim($db_field_type) != trim($field_type) && !empty($db_field_type)) { + if ($this->db_type == "pgsql") { + if (strtolower($field_type) == "uuid") { + $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE uuid USING\n"; + $sql_update .= "CAST(regexp_replace(" . $field_name . ", '([A-Z0-9]{4})([A-Z0-9]{12})', E'\\1-\\2')\n"; + $sql_update .= "AS uuid);\n"; + } else { + //field type has not changed + if ($db_field_type == "integer" && strtolower($field_type) == "serial") { + + } elseif ($db_field_type == "timestamp without time zone" && strtolower($field_type) == "timestamp") { + + } elseif ($db_field_type == "timestamp without time zone" && strtolower($field_type) == "datetime") { + + } elseif ($db_field_type == "timestamp with time zone" && strtolower($field_type) == "timestamptz") { + + } elseif ($db_field_type == "integer" && strtolower($field_type) == "numeric") { + + } elseif ($db_field_type == "character" && strtolower($field_type) == "char") { + + } //field type has changed + else { + switch ($field_type) { + case 'numeric': + $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::numeric;\n"; + break; + case 'timestamp': + $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::timestamp with time zone;\n"; + break; + case 'datetime': + $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::timestamp without time zone;\n"; + break; + case 'timestamptz': + $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::timestamp with time zone;\n"; + break; + case 'boolean': + if ($db_field_type == 'numeric') { + $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE text USING " . $field_name . "::text;\n"; + } + if ($db_field_type == 'text') { + $sql_update .= "UPDATE " . $table_name . " set " . $field_name . " = 'false' where " . $field_name . " = '';\n"; + } + $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::boolean;\n"; + break; + default: + unset($using); + $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . "\n"; + } + } + } + } + if ($this->db_type == "mysql") { + $type = explode("(", $db_field_type); + if ($type[0] == $field_type) { + //do nothing + } elseif ($field_type == "numeric" && $type[0] == "decimal") { + //do nothing + } else { + $sql_update .= "ALTER TABLE " . $table_name . " modify " . $field_name . " " . $field_type . ";\n"; + } + unset($type); + } + if ($this->db_type == "sqlite") { + //a change has been made to the field type + $this->applications[$x]['db'][$y]['rebuild'] = 'true'; } } - unset($field_name); } } - unset($table_name); + } + } elseif (!is_array($row['table']['name'])) { + //create table + $sql_update .= $this->create_table($this->applications, $row['table']['name']); + } + } + } + } + //rebuild and populate the table + foreach ($this->applications as $x => $app) { + if (isset($app['db'])) { + foreach ($app['db'] as $y => $row) { + if (is_array($row['table']['name'])) { + $table_name = $row['table']['name']['text']; + } else { + $table_name = $row['table']['name']; + } + if (!empty($field['rebuild']) && $row['rebuild'] == "true") { + if ($this->db_type == "sqlite") { + //start the transaction + //$sql_update .= "BEGIN TRANSACTION;\n"; + //rename the table + $sql_update .= "ALTER TABLE " . $table_name . " RENAME TO tmp_" . $table_name . ";\n"; + //create the table + $sql_update .= $this->create_table($this->applications, $table_name); + //insert the data into the new table + $sql_update .= $this->insert_into($this->applications, $table_name); + //drop the old table + $sql_update .= "DROP TABLE tmp_" . $table_name . ";\n"; + //commit the transaction + //$sql_update .= "COMMIT;\n"; } } } } + } - //prepare the variables - $sql_update = ''; + // initialize response variable + $response = ''; - //add missing tables and fields - foreach ($this->applications as $x => $app) { + //display results as html + if ($format == "html") { + //show the database type + $response .= "" . $text['header-database_type'] . ": " . $this->db_type . "

    "; + //start the table + $response .= "\n"; + //show the changes + if (!empty($sql_update)) { + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + } + //list all tables + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + //build the html while looping through the app db array + $sql = ''; + foreach ($this->applications as $app) { if (isset($app['db'])) { - foreach ($app['db'] as $y => $row) { + foreach ($app['db'] as $row) { if (is_array($row['table']['name'])) { $table_name = $row['table']['name']['text']; - if ($this->table_exists($row['table']['name']['deprecated'])) { - $row['exists'] = "false"; //testing - if ($this->db_type == "pgsql") { - $sql_update .= "ALTER TABLE " . $row['table']['name']['deprecated'] . " RENAME TO " . $row['table']['name']['text'] . ";\n"; - } - if ($this->db_type == "mysql") { - $sql_update .= "RENAME TABLE " . $row['table']['name']['deprecated'] . " TO " . $row['table']['name']['text'] . ";\n"; - } - if ($this->db_type == "sqlite") { - $sql_update .= "ALTER TABLE " . $row['table']['name']['deprecated'] . " RENAME TO " . $row['table']['name']['text'] . ";\n"; - } - } else { - if ($this->table_exists($row['table']['name']['text'])) { - $row['exists'] = "true"; - } else { - $row['exists'] = "false"; - $sql_update .= $this->create_table($this->applications, $row['table']['name']['text']); - } - } } else { - if ($this->table_exists($row['table']['name'])) { - $row['exists'] = "true"; - } else { - $row['exists'] = "false"; - } $table_name = $row['table']['name']; } + $response .= "\n"; //check if the table exists if ($row['exists'] == "true") { + $response .= "\n"; + $response .= "\n"; + if (count($row['fields']) > 0) { - foreach ($row['fields'] as $z => $field) { + $response .= "\n"; } - } elseif (!is_array($row['table']['name'])) { - //create table - $sql_update .= $this->create_table($this->applications, $row['table']['name']); + } else { + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + } + $response .= "\n"; + } + } + } + //end the list of tables + $response .= "
    \n"; + $response .= "
    \n"; + $response .= "" . $text['label-sql_changes'] . ":
    \n"; + $response .= "
    \n";
    +				$response .= $sql_update;
    +				$response .= "
    \n"; + $response .= "
    \n"; + $response .= "
    " . $text['label-table'] . "" . $text['label-exists'] . "" . $text['label-details'] . "
    " . $table_name . "" . $text['option-true'] . "\n"; + //show the list of columns + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + foreach ($row['fields'] as $field) { if (!empty($field['deprecated']) && $field['deprecated'] == "true") { //skip this field } else { - //get the data type - if (is_array($field['type'])) { - $field_type = $field['type'][$this->db_type]; - } else { - $field_type = $field['type']; - } - //get the field name if (is_array($field['name'])) { $field_name = $field['name']['text']; } else { $field_name = $field['name']; } - - //check if the field exists - // if ($this->column_exists($table_name, $field_name)) { - // $field['exists'] = "true"; - // } - // else { - // $field['exists'] = "false"; - // } - //add or rename fields - if (isset($field['name']['deprecated']) && $this->column_exists($table_name, $field['name']['deprecated'])) { - if ($this->db_type == "pgsql") { - $sql_update .= "ALTER TABLE " . $table_name . " RENAME COLUMN " . $field['name']['deprecated'] . " to " . $field['name']['text'] . ";\n"; - } - if ($this->db_type == "mysql") { - $field_type = str_replace("AUTO_INCREMENT PRIMARY KEY", "", $field_type); - $sql_update .= "ALTER TABLE " . $table_name . " CHANGE " . $field['name']['deprecated'] . " " . $field['name']['text'] . " " . $field_type . ";\n"; - } - if ($this->db_type == "sqlite") { - //a change has been made to the field name - $this->applications[$x]['db'][$y]['rebuild'] = 'true'; - } + if (is_array($field['type'])) { + $field_type = $field['type'][$this->db_type]; } else { - //find missing fields and add them - if ($field['exists'] == "false") { - $sql_update .= "ALTER TABLE " . $table_name . " ADD " . $field_name . " " . $field_type . ";\n"; - } + $field_type = $field['type']; } - - //change the schema data types if needed - //if the data type described in the app_config array is different than the type in the database then update the data type - $db_field_type = $this->column_data_type($table_name, $field_name); - $field_type_array = explode("(", $field_type); - $field_type = $field_type_array[0]; - if (trim($db_field_type) != trim($field_type) && !empty($db_field_type)) { - if ($this->db_type == "pgsql") { - if (strtolower($field_type) == "uuid") { - $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE uuid USING\n"; - $sql_update .= "CAST(regexp_replace(" . $field_name . ", '([A-Z0-9]{4})([A-Z0-9]{12})', E'\\1-\\2')\n"; - $sql_update .= "AS uuid);\n"; - } else { - //field type has not changed - if ($db_field_type == "integer" && strtolower($field_type) == "serial") { - - } else if ($db_field_type == "timestamp without time zone" && strtolower($field_type) == "timestamp") { - - } else if ($db_field_type == "timestamp without time zone" && strtolower($field_type) == "datetime") { - - } else if ($db_field_type == "timestamp with time zone" && strtolower($field_type) == "timestamptz") { - - } else if ($db_field_type == "integer" && strtolower($field_type) == "numeric") { - - } else if ($db_field_type == "character" && strtolower($field_type) == "char") { - - } - //field type has changed - else { - switch ($field_type) { - case 'numeric': - $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::numeric;\n"; - break; - case 'timestamp': - $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::timestamp with time zone;\n"; - break; - case 'datetime': - $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::timestamp without time zone;\n"; - break; - case 'timestamptz': - $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::timestamp with time zone;\n"; - break; - case 'boolean': - if ($db_field_type == 'numeric') { - $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE text USING " . $field_name . "::text;\n"; - } - if ($db_field_type == 'text') { - $sql_update .= "UPDATE " . $table_name . " set " . $field_name . " = 'false' where " . $field_name . " = '';\n"; - } - $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . " USING " . $field_name . "::boolean;\n"; - break; - default: unset($using); - $sql_update .= "ALTER TABLE " . $table_name . " ALTER COLUMN " . $field_name . " TYPE " . $field_type . "\n"; - } - } - } - } - if ($this->db_type == "mysql") { - $type = explode("(", $db_field_type); - if ($type[0] == $field_type) { - //do nothing - } else if ($field_type == "numeric" && $type[0] == "decimal") { - //do nothing - } else { - $sql_update .= "ALTER TABLE " . $table_name . " modify " . $field_name . " " . $field_type . ";\n"; - } - unset($type); - } - if ($this->db_type == "sqlite") { - //a change has been made to the field type - $this->applications[$x]['db'][$y]['rebuild'] = 'true'; - } + $response .= "\n"; + $response .= "\n"; + $response .= "\n"; + if ($field['exists'] == "true") { + $response .= "\n"; + $response .= "\n"; + } else { + $response .= "\n"; + $response .= "\n"; } + $response .= "\n"; } } + $response .= "
    " . $text['label-name'] . "" . $text['label-type'] . "" . $text['label-exists'] . "
    " . $field_name . "" . $field_type . "" . $text['option-true'] . " " . $text['option-false'] . " 
    \n"; + $response .= "
    $table_name" . $text['label-exists'] . "
    " . $text['option-false'] . "
     
    \n"; + $response .= "
    \n"; + } + + //loop line by line through all the lines of sql code + $x = 0; + if (empty($sql_update) && $format == "text") { + $response .= " " . $text['label-schema'] ?? '' . ": " . $text['label-no_change'] . "\n"; + } else { + if ($format == "text") { + $response .= " " . $text['label-schema'] . "\n"; + } + //$this->db->beginTransaction(); + $update_array = explode(";", $sql_update); + if (is_array($update_array) && count($update_array)) { + //drop views so that alter table statements complete + $result = $this->database->views('drop'); + + foreach ($update_array as $sql) { + if (strlen(trim($sql))) { + try { + $this->database->execute(trim($sql), null); + if ($format == "text") { + $response .= " $sql;\n"; + } + } catch (PDOException $error) { + $response .= " error: " . $error->getMessage() . " sql: $sql\n"; } } } } - //rebuild and populate the table - foreach ($this->applications as $x => $app) { - if (isset($app['db'])) { + //$this->db->commit(); + $response .= "\n"; + unset($sql_update, $sql); + } + + //refresh each postgresql subscription with its publication + if ($this->db_type == "pgsql") { + //get the list of postgresql subscriptions + $sql = "select subname from pg_subscription; "; + $pg_subscriptions = $this->database->select($sql, null, 'all'); + unset($sql, $parameters); + + //refresh each subscription publication + foreach ($pg_subscriptions as $row) { + $sql = "ALTER SUBSCRIPTION " . $row['subname'] . " REFRESH PUBLICATION;"; + $response .= $sql; + $this->database->execute($sql); + } + } + + //create views so that alter table statements complete + $this->database->views('create'); + + //handle response + return $response; + + } + + //get the table information + + /** + * Checks if a table exists in the database. + * + * This method determines whether a specified table exists in the database, + * based on the current database type and available schema information. + * + * @param string $table_name The name of the table to check for existence. + * + * @return bool True if the table exists, false otherwise. + */ + private function table_exists($table_name) { + if ($this->db_type == 'pgsql') { + if (isset($this->schema_info[$table_name])) { + return true; + } else { + return false; + } + } + if ($this->db_type == 'sqlite' || $this->db_type == 'msyql') { + $sql = "select count(*) from $table_name "; + $result = $this->database->execute($sql, null); + if ($result > 0) { + return true; //table exists + } else { + return false; //table doesn't exist + } + } + } + + //database table exists + + /** + * Check if a column exists in the specified table. + * + * This method performs database-specific checks to determine whether the given + * table_name and column_name exist. The method returns true if the column is found, + * and false otherwise. + * + * @param string $table_name The name of the table to check for the column. + * @param string $column_name The name of the column to check for existence. + * + * @return bool True if the column exists, false otherwise. + */ + public function column_exists($table_name, $column_name) { + if (empty($table_name)) { + return false; + } + if (empty($column_name)) { + return false; + } + if ($this->db_type == "sqlite") { + $table_info = $this->table_info($table_name); + if ($this->sqlite_column_exists($table_info, $column_name)) { + return true; + } else { + return false; + } + } + if ($this->db_type == "pgsql") { + if (!isset($this->schema_info[$table_name])) { + return false; + } + foreach ($this->schema_info[$table_name] as $row) { + if ($row['column_name'] == $column_name) { + return true; + } + } + return false; + } + if ($this->db_type == "mysql") { + //$sql .= "SELECT * FROM information_schema.COLUMNS where TABLE_SCHEMA = '".$this->db_name."' and TABLE_NAME = '$table_name' and COLUMN_NAME = '$column_name' "; + $sql = "show columns from $table_name where field = '$column_name' "; + } + + if ($sql) { + $prep_statement = $this->database->db->prepare($sql); + $prep_statement->execute(); + $result = $prep_statement->fetchAll(PDO::FETCH_NAMED); + unset($prep_statement); + if (!$result) { + return false; + } + if (count($result) > 0) { + return true; + } else { + return false; + } + } + } + + //database type + + /** + * Retrieves table information. + * + * This method fetches the specified table's schema from the database. + * + * @param string $table_name The name of the table for which to retrieve information. + * + * @return array|null Table schema information, or null if unable to retrieve. + */ + public function table_info($table_name) { + if (empty($table_name)) { + return false; + } + if ($this->db_type == "pgsql") { + if (!isset($this->schema_info[$table_name])) { + return false; + } + return $this->schema_info[$table_name]; + } + if ($this->db_type == "sqlite") { + $sql = "PRAGMA table_info(" . $table_name . ");"; + } + if ($this->db_type == "mysql") { + $sql = "describe " . $table_name . ";"; + } + $prep_statement = $this->database->db->prepare($sql); + $prep_statement->execute(); + return $prep_statement->fetchAll(PDO::FETCH_ASSOC); + } + + //sqlite column exists + + /** + * Checks if a column exists in the provided table information. + * + * This method iterates over the table_info array and checks for the existence of + * the specified column_name. If found, it returns true; otherwise, it returns false. + * + * @param array $table_info An array containing information about the columns in a table. + * @param string $column_name The name of the column to check for existence. + * + * @return bool True if the column exists, false otherwise. + */ + private function sqlite_column_exists($table_info, $column_name) { + foreach ($table_info as $key => $row) { + if ($row['name'] == $column_name) { + return true; + } + } + return false; + } + + //database column data type + + /** + * Creates a table based on the provided app and table name. + * + * This method iterates through each app in the apps array, and then checks for + * a table with the specified name within that app. If a matching table is found, + * it generates an SQL statement to create the table. + * + * @param array $apps An array of applications, where each application has a 'db' + * sub-array containing information about tables in that database. + * @param string $table The name of the table to be created. + * + * @return string|null The SQL statement to create the table, or null if no match is found. + */ + public function create_table($apps, $table) { + if (empty($apps)) { + return false; + } + if (is_array($apps)) { + foreach ($apps as $x => $app) { + if (!empty($app['db']) && is_array($app['db'])) { foreach ($app['db'] as $y => $row) { - if (is_array($row['table']['name'])) { + if (!empty($row['table']['name']) && is_array($row['table']['name'])) { $table_name = $row['table']['name']['text']; } else { $table_name = $row['table']['name']; } - if (!empty($field['rebuild']) && $row['rebuild'] == "true") { - if ($this->db_type == "sqlite") { - //start the transaction - //$sql_update .= "BEGIN TRANSACTION;\n"; - //rename the table - $sql_update .= "ALTER TABLE " . $table_name . " RENAME TO tmp_" . $table_name . ";\n"; - //create the table - $sql_update .= $this->create_table($this->applications, $table_name); - //insert the data into the new table - $sql_update .= $this->insert_into($this->applications, $table_name); - //drop the old table - $sql_update .= "DROP TABLE tmp_" . $table_name . ";\n"; - //commit the transaction - //$sql_update .= "COMMIT;\n"; + if ($table_name == $table) { + $sql = "CREATE TABLE " . $table_name . " (\n"; + (int)$field_count = 0; + if (!empty($row['fields']) && is_array($row['fields'])) { + foreach ($row['fields'] as $field) { + if (!empty($field['deprecated']) && $field['deprecated'] == "true") { + //skip this row + } else { + if ($field_count > 0) { + $sql .= ",\n"; + } + if (!empty($field['name']) && is_array($field['name'])) { + $sql .= $field['name']['text'] . " "; + } else { + $sql .= $field['name'] . " "; + } + if (!empty($field['type']) && is_array($field['type'])) { + $sql .= $field['type'][$this->db_type]; + } else { + $sql .= $field['type']; + } + if (!empty($field['key']['type']) && $field['key']['type'] == "primary") { + $sql .= " PRIMARY KEY"; + } + $field_count++; + } + } } + $sql .= ");\n"; + return $sql; } } } } + } + } - // initialize response variable - $response = ''; + //database create table - //display results as html - if ($format == "html") { - //show the database type - $response .= "" . $text['header-database_type'] . ": " . $this->db_type . "

    "; - //start the table - $response .= "\n"; - //show the changes - if (!empty($sql_update)) { - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; + /** + * Returns the data type of a column. + * + * This method retrieves the table info for the specified table name and then calls the + * data_type() method to get the data type of the specified column. + * + * @param string $table_name The name of the table. + * @param string $column_name The name of the column. + * + * @return mixed The data type of the column. + */ + private function column_data_type($table_name, $column_name) { + $table_info = $this->table_info($table_name); + return $this->data_type($table_info, $column_name); + } + + //database insert + + /** + * Retrieves the data type of a column in the database. + * + * This method checks the data type based on the current database type and + * returns the corresponding value from the table_info array. + * + * @param array $table_info An array containing table information. + * @param string $column_name The name of the column to retrieve the data type for. + * + * @return mixed The data type of the specified column, or null if not found. + */ + private function data_type($table_info, $column_name) { + if ($this->db_type == "sqlite") { + foreach ($table_info as $key => $row) { + if ($row['name'] == $column_name) { + return $row['type']; } - //list all tables - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; - //build the html while looping through the app db array - $sql = ''; - foreach ($this->applications as $app) { - if (isset($app['db'])) { - foreach ($app['db'] as $row) { - if (is_array($row['table']['name'])) { - $table_name = $row['table']['name']['text']; - } else { - $table_name = $row['table']['name']; + } + } + if ($this->db_type == "pgsql") { + foreach ($table_info as $key => $row) { + if ($row['column_name'] == $column_name) { + return $row['data_type']; + } + } + } + if ($this->db_type == "mysql") { + foreach ($table_info as $key => $row) { + if ($row['Field'] == $column_name) { + return $row['Type']; + } + } + } + } + + //datatase schema + + /** + * Inserts data from temporary tables into a specified database table. + * + * This method iterates through the provided applications, database definitions, + * and temporary tables to construct SQL INSERT statements. The constructed SQL + * statements are returned for execution. + * + * @param array $apps An array of application configurations. + * @param string $table The name of the target database table. + * + * @return string The constructed SQL INSERT statement as a string. + */ + private function insert_into($apps, $table) { + foreach ($apps as $x => $app) { + foreach ($app['db'] as $y => $row) { + if ($row['table']['name'] == $table) { + $sql = "INSERT INTO " . $row['table']['name'] . " ("; + $field_count = 0; + foreach ($row['fields'] as $field) { + if (!empty($field['deprecated']) && $field['deprecated'] == "true") { + //skip this field + } else { + if ($field_count > 0) { + $sql .= ","; } - $response .= "\n"; - - //check if the table exists - if ($row['exists'] == "true") { - $response .= "\n"; - $response .= "\n"; - - if (count($row['fields']) > 0) { - $response .= "\n"; + } else { + $sql .= $field['name']['text']; } } else { - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; + $sql .= $field['name']; } - $response .= "\n"; + $field_count++; } } - } - //end the list of tables - $response .= "
    \n"; - $response .= "
    \n"; - $response .= "" . $text['label-sql_changes'] . ":
    \n"; - $response .= "
    \n";
    -					$response .= $sql_update;
    -					$response .= "
    \n"; - $response .= "
    \n"; - $response .= "
    " . $text['label-table'] . "" . $text['label-exists'] . "" . $text['label-details'] . "
    " . $table_name . "" . $text['option-true'] . "\n"; - //show the list of columns - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; - foreach ($row['fields'] as $field) { - if (!empty($field['deprecated']) && $field['deprecated'] == "true") { - //skip this field + if (is_array($field['name'])) { + $sql .= $field['name']['text']; + } else { + $sql .= $field['name']; + } + $field_count++; + } + } + $sql .= ")\n"; + $sql .= "SELECT "; + $field_count = 0; + foreach ($row['fields'] as $field) { + if (!empty($field['deprecated']) && $field['deprecated'] == "true") { + //skip this field + } else { + if ($field_count > 0) { + $sql .= ","; + } + if (is_array($field['name'])) { + if ($field['exists'] == "false") { + if (is_array($field['name']['deprecated'])) { + $found = false; + foreach ($field['name']['deprecated'] as $row) { + if ($this->column_exists('tmp_' . $table, $row)) { + $sql .= $row; + $found = true; + break; + } + } + if (!$found) { + $sql .= "''"; + } + } else { + if ($this->column_exists('tmp_' . $table, $field['name']['deprecated'])) { + $sql .= $field['name']['deprecated']; } else { - if (is_array($field['name'])) { - $field_name = $field['name']['text']; - } else { - $field_name = $field['name']; - } - if (is_array($field['type'])) { - $field_type = $field['type'][$this->db_type]; - } else { - $field_type = $field['type']; - } - $response .= "\n"; - $response .= "\n"; - $response .= "\n"; - if ($field['exists'] == "true") { - $response .= "\n"; - $response .= "\n"; - } else { - $response .= "\n"; - $response .= "\n"; - } - $response .= "\n"; + $sql .= "''"; } } - $response .= "
    " . $text['label-name'] . "" . $text['label-type'] . "" . $text['label-exists'] . "
    " . $field_name . "" . $field_type . "" . $text['option-true'] . " " . $text['option-false'] . " 
    \n"; - $response .= "
    $table_name" . $text['label-exists'] . "
    " . $text['option-false'] . "
     
    \n"; - $response .= "
    \n"; - } - - //loop line by line through all the lines of sql code - $x = 0; - if (empty($sql_update) && $format == "text") { - $response .= " " . $text['label-schema'] ?? '' . ": " . $text['label-no_change'] . "\n"; - } else { - if ($format == "text") { - $response .= " " . $text['label-schema'] . "\n"; - } - //$this->db->beginTransaction(); - $update_array = explode(";", $sql_update); - if (is_array($update_array) && count($update_array)) { - //drop views so that alter table statements complete - $result = $this->database->views('drop'); - - foreach ($update_array as $sql) { - if (strlen(trim($sql))) { - try { - $this->database->execute(trim($sql), null); - if ($format == "text") { - $response .= " $sql;\n"; - } - } catch (PDOException $error) { - $response .= " error: " . $error->getMessage() . " sql: $sql\n"; - } - } - } - } - //$this->db->commit(); - $response .= "\n"; - unset($sql_update, $sql); - } - - //refresh each postgresql subscription with its publication - if ($this->db_type == "pgsql") { - //get the list of postgresql subscriptions - $sql = "select subname from pg_subscription; "; - $pg_subscriptions = $this->database->select($sql, null, 'all'); - unset($sql, $parameters); - - //refresh each subscription publication - foreach ($pg_subscriptions as $row) { - $sql = "ALTER SUBSCRIPTION " . $row['subname'] . " REFRESH PUBLICATION;"; - $response .= $sql; - $this->database->execute($sql); + $sql .= " FROM tmp_" . $table . ";\n"; + return $sql; } } - - //create views so that alter table statements complete - $this->database->views('create'); - - //handle response - return $response; - - } //end function - } + } + } //end function +} //example use //$schema = new schema(); diff --git a/resources/classes/service.php b/resources/classes/service.php index ecbaab7e72..c69323b640 100644 --- a/resources/classes/service.php +++ b/resources/classes/service.php @@ -28,88 +28,69 @@ /** * Service class + * * @version 1.00 - * @author Tim Fry + * @author Tim Fry */ abstract class service { const VERSION = "1.00"; - - /** - * Track the internal loop. It is recommended to use this variable to control the loop inside the run function. See the example - * below the class for a more complete explanation - * @var bool - */ - protected $running; - /** * current debugging level for output to syslog + * * @var int Syslog level */ protected static $log_level = LOG_NOTICE; - /** * config object + * * @var config config object */ protected static $config; - /** * Holds the parsed options from the command line + * * @var array */ protected static $parsed_command_options; - - /** - * Operating System process identification file - * @var string - */ - private static $pid_file = ""; - /** * Cli Options Array + * * @var array */ protected static $available_command_options = []; - /** * Holds the configuration file location + * * @var string */ protected static $config_file = ""; - /** * Fork the service to it's own process ID + * * @var bool */ protected static $daemon_mode = false; - /** * Suppress the timestamp * Used to suppress the timestamp in syslog + * * @var bool */ protected static $show_timestamp_log = false; - /** - * Child classes must provide a mechanism to reload settings + * Operating System process identification file + * + * @var string */ - abstract protected function reload_settings(): void; - + private static $pid_file = ""; /** - * Method to start the child class internal loop + * Track the internal loop. It is recommended to use this variable to control the loop inside the run function. See + * the example below the class for a more complete explanation + * + * @var bool */ - abstract public function run(): int; - - /** - * Display version notice - */ - abstract protected static function display_version(): void; - - /** - * Called when the display_help_message is run in the base class for extra command line parameter explanation - */ - abstract protected static function set_command_options(); + protected $running; /** * Open a log when created. @@ -123,27 +104,6 @@ abstract class service { openlog('[php][' . self::class . ']', LOG_CONS | LOG_NDELAY | LOG_PID, LOG_DAEMON); } - /** - * Ensures the correct PID file is unlinked when this process terminates. - * - * This method is called automatically by PHP when an object's reference count reaches zero, - * or when it's explicitly destroyed using unset(). It checks if the process is still running - * and unlinks the corresponding PID file. Finally, it ensures that any open log connections - * are properly closed before the script exits. - * - * @return void - */ - public function __destruct() { - //ensure we unlink the correct PID file if needed - if (self::is_running()) { - unlink(self::$pid_file); - self::log("Initiating Shutdown...", LOG_NOTICE); - $this->running = false; - } - //this should remain the last statement to execute before exit - closelog(); - } - /** * Shutdown the currently running process gracefully. */ @@ -162,66 +122,214 @@ abstract class service { } } - // register signal handlers - private function register_signal_handlers() { - // Allow the calls to be made while the main loop is running - pcntl_async_signals(true); - - // A signal listener to reload the service for any config changes in the database - pcntl_signal(SIGUSR1, [$this, 'reload_settings']); - pcntl_signal(SIGHUP, [$this, 'reload_settings']); - - // A signal listener to stop the service - pcntl_signal(SIGUSR2, [self::class, 'shutdown']); - pcntl_signal(SIGTERM, [self::class, 'shutdown']); + /** + * Checks if any service process is running. + * + * @return bool True if a service process is found, false otherwise + */ + public static function is_any_running(): bool { + return self::get_service_pid() !== false; } /** - * Extracts the short options from the cli options array and returns a string. The resulting string must - * return a single string with all options in the string such as 'rxc:'. - * This can be overridden by the child class. - * - * @return string + * Sends the shutdown signal to the service using a posix signal. + *

    NOTE:
    + * The signal will not be received from the service if the + * command is sent from a user that has less privileges then + * the running service. For example, if the service is started + * by user root and then the command line option '-r' is given + * as user www-data, the service will not receive this signal + * because the OS will not allow the signal to be passed to a + * more privileged user due to security concerns. This would + * be the main reason why you must run a 'systemctl' or a + * 'service' command as root user. It is possible to start the + * service with user www-data and then the web UI would in fact + * be able to send the reload signal to the running service.

    */ - protected static function get_short_options(): string { - return implode('' , array_map(function ($option) { return $option['short_option']; }, self::$available_command_options)); - } - - /** - * Extracts the long options from the cli options array and returns an array. The resulting array must - * return a single dimension array with an integer indexed key but does not have to be sequential order. - * This can be overridden by the child class. - * - * @return array - */ - protected static function get_long_options(): array { - return array_map(function ($option) { return $option['long_option']; }, self::$available_command_options); - } - - /** - * Retrieves the callback functions associated with the cli options array. - * - * @param string $set_option The set option to match against available options - * - * @return array An array of callback functions that need to be called for the matched option - */ - protected static function get_user_callbacks_from_available_options(string $set_option): array { - //match the available option to the set option and return the callback function that needs to be called - foreach(self::$available_command_options as $option) { - $short_option = $option['short_option'] ?? ''; - if (str_ends_with($short_option, ':')) { - $short_option = rtrim($short_option, ':'); - } - $long_option = $option['long_option'] ?? ''; - if (str_ends_with($long_option, ':')) { - $long_option = rtrim($long_option, ':'); - } - if ($short_option === $set_option || - $long_option === $set_option) { - return $option['functions'] ?? [$option['function']] ?? []; + public static function send_signal($posix_signal) { + $signal_name = ""; + switch ($posix_signal) { + case 1: //SIGHUP + case 10: //SIGUSR1 + $signal_name = "Reload"; + break; + case 12: //SIGUSR2 + case 15: //SIGTERM + $signal_name = "Shutdown"; + break; + } + $pid = self::get_service_pid(); + if ($pid === false) { + self::log("service not running", LOG_EMERG); + } else { + if (posix_kill((int)$pid, $posix_signal)) { + echo "Sent $signal_name\n"; + } else { + $err = posix_strerror(posix_get_last_error()); + echo "Failed to send $signal_name: $err\n"; } } - return []; + } + + /** + * Write a standard copyright notice to the console + * + * @return void + */ + public static function display_copyright(): void { + echo "FusionPBX\n"; + echo "Version: MPL 1.1\n"; + echo "\n"; + echo "The contents of this file are subject to the Mozilla Public License Version\n"; + echo "1.1 (the \"License\"); you may not use this file except in compliance with\n"; + echo "the License. You may obtain a copy of the License at\n"; + echo "http://www.mozilla.org/MPL/\n"; + echo "\n"; + echo "Software distributed under the License is distributed on an \"AS IS\" basis,\n"; + echo "WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License\n"; + echo "for the specific language governing rights and limitations under the\n"; + echo "License.\n"; + echo "\n"; + echo "The Original Code is FusionPBX\n"; + echo "\n"; + echo "The Initial Developer of the Original Code is\n"; + echo "Mark J Crane \n"; + echo "Portions created by the Initial Developer are Copyright (C) 2008-2023\n"; + echo "the Initial Developer. All Rights Reserved.\n"; + echo "\n"; + echo "Contributor(s):\n"; + echo "Mark J Crane \n"; + echo "Tim Fry \n"; + echo "\n"; + } + + /** + * Sends a reload signal to running services, or exits if no service is running. + * + * @return void + */ + public static function send_reload() { + if (self::is_any_running()) { + self::send_signal(10); + } else { + die("Service Not Started\n"); + } + exit(); + } + + /** + * Set to foreground when started + */ + public static function enable_daemon_mode() { + self::$daemon_mode = true; + self::$show_timestamp_log = false; + } + + // register signal handlers + + /** + * Set the configuration file to be used by FusionPBX. + * + * @param string $file The path to the configuration file (default: '/etc/fusionpbx/config.conf') + * + * @return void + */ + public static function set_config_file(string $file = '/etc/fusionpbx/config.conf') { + if (empty(self::$config_file)) { + self::$config_file = $file; + } + self::$config = new config(self::$config_file); + } + + /** + * Appends a command option to the list of available options. + * + * @param command_option $option The command option to append. + * + * @return int The index where the option was appended. + */ + public static function append_command_option(command_option $option): int { + $index = count(self::$available_command_options); + self::$available_command_options[$index] = $option->to_array(); + return $index; + } + + /** + * Add a new command option to the available options list. + * + * @param string $short_option Short option (e.g., 'h' for '-h') + * @param string $long_option Long option (e.g., 'help' for '--help') + * @param string $description Brief description of the option + * @param string $short_description Short description of the short option (optional) + * @param string $long_description Long description of the long option (optional) + * @param mixed[] ...$callback Callback function(s) associated with this option + * + * @return int Index of the newly added command option in self::$available_command_options array + */ + public static function add_command_option(string $short_option, string $long_option, string $description, string $short_description = '', string $long_description = '', ...$callback): int { + //use the option as the description if not filled in + if (empty($short_description)) { + $short_description = '-' . $short_option; + if (str_ends_with($short_option, ':')) { + $short_description .= " "; + } + } + if (empty($long_description)) { + $long_description = '-' . $long_option; + if (str_ends_with($long_option, ':')) { + $long_description .= " "; + } + } + $index = count(self::$available_command_options); + self::$available_command_options[$index]['short_option'] = $short_option; + self::$available_command_options[$index]['long_option'] = $long_option; + self::$available_command_options[$index]['description'] = $description; + self::$available_command_options[$index]['short_description'] = $short_description; + self::$available_command_options[$index]['long_description'] = $long_description; + self::$available_command_options[$index]['functions'] = $callback; + return $index; + } + + /** + * Creates a new instance of the service class, initializing it and returning the object. + * + * @return self A new instance of the service class. + */ + public static function create(): self { + //can only start from command line + defined('STDIN') or die('Unauthorized'); + + //parse the cli options and store them statically + self::parse_service_command_options(); + + //fork process + if (self::$daemon_mode) { + //force launching in a separate process + if ($pid = pcntl_fork()) { + exit; + } + + if ($cid = pcntl_fork()) { + exit; + } + } + + //create the config object if not already created + if (self::$config === null) { + self::$config = new config(self::$config_file); + } + + //get the name of child object + $class = self::base_class_name(); + + //create the child object + $service = new $class(); + + //initialize the service + $service->init(); + + //return the initialized object + return $service; } /** @@ -278,7 +386,7 @@ abstract class service { //check for more than one function to be called is permitted if (is_array($funcs)) { //call each one - foreach($funcs as $func) { + foreach ($funcs as $func) { //use the best method to call the function self::call_function($func, $option_value); } @@ -291,366 +399,22 @@ abstract class service { } // - // Calls a function using the best suited PHP method // - private static function call_function($function, $args) { - if ($function === 'exit') { - //check for exit - exit($args); - } elseif ($function instanceof Closure || function_exists($function)) { - //globally available function or closure - $function($args); - } else { - static::$function($args); - } - } - - /** - * Checks the file system for a pid file that matches the process ID from this running instance - * - * @return bool true if pid exists and false if not - */ - public static function is_running(): bool { - return posix_getpid() === self::get_service_pid(); - } - - /** - * Checks if any service process is running. - * - * @return bool True if a service process is found, false otherwise - */ - public static function is_any_running(): bool { - return self::get_service_pid() !== false; - } - - /** - * Returns the process ID of the service if it exists and is running. - * - * @return int|false The process ID of the service, or false if it does not exist or is not running. - */ - protected static function get_service_pid() { - if (file_exists(static::get_pid_filename())) { - $pid = file_get_contents(static::get_pid_filename()); - if (function_exists('posix_getsid')) { - if (posix_getsid($pid) !== false) { - //return the pid for reloading configuration - return intval($pid); - } - } else { - if (file_exists('/proc/' . $pid)) { - //return the pid for reloading configuration - return intval($pid); - } - } - } - return false; - } - - /** - * Create an operating system PID file removing any existing PID file - */ - private function create_service_pid() { - // Set the pid filename - $basename = basename(self::$pid_file, '.pid'); - $pid = getmypid(); - - // Remove the old pid file - if (file_exists(self::$pid_file)) { - if (is_writable(self::$pid_file)) { - unlink(self::$pid_file); - } else { - throw new \RuntimeException("Unable to write to PID file " . self::$pid_file, 73); //Unix error code 73 - unable to write/create file - } - } - - // Show the details to the user - self::log("Starting up..."); - self::log("Mode : " . (self::$daemon_mode ? "Daemon" : "Foreground"), LOG_INFO); - self::log("Service : $basename", LOG_INFO); - self::log("Process ID: $pid", LOG_INFO); - self::log("PID File : " . self::$pid_file, LOG_INFO); - self::log("Log level : " . self::log_level_to_string(self::$log_level), LOG_INFO); - self::log("Timestamps: " . (self::$show_timestamp_log ? "Yes" : "No"), LOG_INFO); - - // Save the pid file - $success = file_put_contents(self::$pid_file, $pid); - if ($success === false) { - throw new \RuntimeException("Failed writing to PID file " . self::$pid_file, 74); //Unix error code 74 - I/O error - } - } - - /** - * Creates the service directory to store the PID - * - * @return void - * @throws Exception thrown when the service directory is unable to be created - */ - private function create_service_directory() { - //make sure the /var/run/fusionpbx directory exists - if (!file_exists('/var/run/fusionpbx')) { - $result = mkdir('/var/run/fusionpbx', 0777, true); - if (!$result) { - throw new Exception('Failed to create /var/run/fusionpbx'); - } - } - } - - /** - * Parses the debug level to an integer and stores it in the class for syslog use - * - * @param string $debug_level Debug level with any of the Linux system log levels - * - * @return void - */ - protected static function set_debug_level(string $debug_level) { - // Map user input log level to syslog constant - switch ($debug_level) { - case '0': - case 'emergency': - self::$log_level = LOG_EMERG; // Hardware failures - break; - case '1': - case 'alert': - self::$log_level = LOG_ALERT; // Loss of network connection or a condition that should be corrected immediately - break; - case '2': - case 'critical': - self::$log_level = LOG_CRIT; // Condition like low disk space - break; - case '3': - case 'error': - self::$log_level = LOG_ERR; // Database query failure, file not found - break; - case '4': - case 'warning': - self::$log_level = LOG_WARNING; // Deprecated function usage, approaching resource limits - break; - case '5': - case 'notice': - self::$log_level = LOG_NOTICE; // Normal conditions - break; - case '6': - case 'info': - self::$log_level = LOG_INFO; // Informational - break; - case '7': - case 'debug': - self::$log_level = LOG_DEBUG; // Debugging - break; - default: - self::$log_level = LOG_NOTICE; // Default to NOTICE if invalid level - } - - // When we are using LOG_DEBUG there is a high chance we are logging to the console - // directly without systemctl so enable the timestamps by default - if (self::$log_level === LOG_DEBUG && !self::$daemon_mode) { - self::show_timestamp(); - } - } - - /** - * Converts a log level integer to its corresponding string representation. - * - * @param int $level The log level as an integer, defaults to LOG_NOTICE (5). - * - * @return string The log level as a string. - */ - private static function log_level_to_string(int $level = LOG_NOTICE): string { - switch ($level){ - case 0: - return 'EMERGENCY'; - case 1: - return 'ALERT'; - case 2: - return 'CRITICAL'; - case 3: - return 'ERROR'; - case 4: - return 'WARNING'; - case 5: - return 'NOTICE'; - case 6: - return 'INFO'; - case 7: - return 'DEBUG'; - default: - return 'INFO'; - } - } - - /** - * Logs the current and peak memory usage of the application at the INFO level. - * - * @return void - */ - protected static function show_mem_usage() { - //current memory - $memory_usage = memory_get_usage(); - //peak memory - $memory_peak = memory_get_peak_usage(); - self::log('Current memory: ' . round($memory_usage / 1024) . " KB", LOG_INFO); - self::log('Peak memory: ' . round($memory_peak / 1024) . " KB", LOG_INFO); - } - - /** - * Logs to the system log or console when running in foreground - * - * @param string $message Message to display in the system log or console when running in foreground - * @param int $level (Optional) Level to use for logging to the console or daemon. Default value is LOG_NOTICE - * - * @return void - */ - protected static function log(string $message, int $level = LOG_NOTICE) { - // Check if we need to show the message - if ($level <= self::$log_level) { - // When not in daemon mode we log to console directly - if (!self::$daemon_mode) { - $level_as_string = self::log_level_to_string($level); - if (!self::$show_timestamp_log) { - echo "[$level_as_string] $message\n"; - } else { - $time = date('Y-m-d H:i:s'); - echo "[$time][$level_as_string] $message\n"; - } - } else { - // Log the message to syslog - syslog($level, 'fusionpbx[' . posix_getpid() . ']: ['.static::class.'] '.$message); - } - } - } - - /** - * Returns a file safe class name with \ from namespaces converted to _ - * @return string file safe name - */ - protected static function base_file_name(): string { - return str_replace('\\', "_", static::class); - } - - /** - * Returns only the name of the class without namespace - * @return string base class name - */ - protected static function base_class_name(): string { - $class_and_namespace = explode('\\', static::class); - return array_pop($class_and_namespace); - } - - /** - * Write a standard copyright notice to the console - * - * @return void - */ - public static function display_copyright(): void { - echo "FusionPBX\n"; - echo "Version: MPL 1.1\n"; - echo "\n"; - echo "The contents of this file are subject to the Mozilla Public License Version\n"; - echo "1.1 (the \"License\"); you may not use this file except in compliance with\n"; - echo "the License. You may obtain a copy of the License at\n"; - echo "http://www.mozilla.org/MPL/\n"; - echo "\n"; - echo "Software distributed under the License is distributed on an \"AS IS\" basis,\n"; - echo "WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License\n"; - echo "for the specific language governing rights and limitations under the\n"; - echo "License.\n"; - echo "\n"; - echo "The Original Code is FusionPBX\n"; - echo "\n"; - echo "The Initial Developer of the Original Code is\n"; - echo "Mark J Crane \n"; - echo "Portions created by the Initial Developer are Copyright (C) 2008-2023\n"; - echo "the Initial Developer. All Rights Reserved.\n"; - echo "\n"; - echo "Contributor(s):\n"; - echo "Mark J Crane \n"; - echo "Tim Fry \n"; - echo "\n"; - } - - /** - * Sends the shutdown signal to the service using a posix signal. - *

    NOTE:
    - * The signal will not be received from the service if the - * command is sent from a user that has less privileges then - * the running service. For example, if the service is started - * by user root and then the command line option '-r' is given - * as user www-data, the service will not receive this signal - * because the OS will not allow the signal to be passed to a - * more privileged user due to security concerns. This would - * be the main reason why you must run a 'systemctl' or a - * 'service' command as root user. It is possible to start the - * service with user www-data and then the web UI would in fact - * be able to send the reload signal to the running service.

    - */ - public static function send_signal($posix_signal) { - $signal_name = ""; - switch ($posix_signal) { - case 1: //SIGHUP - case 10: //SIGUSR1 - $signal_name = "Reload"; - break; - case 12: //SIGUSR2 - case 15: //SIGTERM - $signal_name = "Shutdown"; - break; - } - $pid = self::get_service_pid(); - if ($pid === false) { - self::log("service not running", LOG_EMERG); - } else { - if (posix_kill((int) $pid, $posix_signal) ) { - echo "Sent $signal_name\n"; - } else { - $err = posix_strerror(posix_get_last_error()); - echo "Failed to send $signal_name: $err\n"; - } - } - } - - /** - * Displays a help message for the available command options. - */ - protected static function display_help_message(): void { - //get the classname of the child class - $class_name = self::base_class_name(); - - //get the widest options for proper alignment - $width_short = max(array_map(function ($arr) { return strlen($arr['short_description'] ?? ''); }, self::$available_command_options)); - $width_long = max(array_map(function ($arr) { return strlen($arr['long_description' ] ?? ''); }, self::$available_command_options)); - - //display usage help using the class name of child - echo "Usage: php $class_name [options]\n"; - - //display the options aligned to the widest short and long options - echo "Options:\n"; - foreach (self::$available_command_options as $option) { - printf("%-{$width_short}s %-{$width_long}s %s\n", - $option['short_description'], - $option['long_description'], - $option['description'] - ); - } - } - - /** - * Sends a reload signal to running services, or exits if no service is running. - * - * @return void - */ - public static function send_reload() { - if (self::is_any_running()) { - self::send_signal(10); - } else { - die("Service Not Started\n"); - } - exit(); - } - - // - // Options built-in to the base service class. These can be overridden with the child class - // or they can be extended using the array // + + /** + * Returns an array of base command options. + * + * This method constructs an array of help options for the command line interface, + * including short and long option descriptions, functions that these options + * trigger, and other metadata. The resulting array can be used to provide help + * messages or determine which functions are available based on the input. + * + * @return array An associative array where each key represents a help option and + * its corresponding value is an associative array containing + * short_option, long_option, description, short_description, + * long_description, and functions keys. + */ private static function base_command_options(): array { //put the display for help in an array so we can calculate width $help_options = []; @@ -727,88 +491,80 @@ abstract class service { } /** - * Enables timestamp logging. + * Called when the display_help_message is run in the base class for extra command line parameter explanation + */ + abstract protected static function set_command_options(); + + /** + * Extracts the short options from the cli options array and returns a string. The resulting string must + * return a single string with all options in the string such as 'rxc:'. + * This can be overridden by the child class. + * + * @return string + */ + protected static function get_short_options(): string { + return implode('', array_map(function ($option) { + return $option['short_option']; + }, self::$available_command_options)); + } + + /** + * Extracts the long options from the cli options array and returns an array. The resulting array must + * return a single dimension array with an integer indexed key but does not have to be sequential order. + * This can be overridden by the child class. + * + * @return array + */ + protected static function get_long_options(): array { + return array_map(function ($option) { + return $option['long_option']; + }, self::$available_command_options); + } + + /** + * Retrieves the callback functions associated with the cli options array. + * + * @param string $set_option The set option to match against available options + * + * @return array An array of callback functions that need to be called for the matched option + */ + protected static function get_user_callbacks_from_available_options(string $set_option): array { + //match the available option to the set option and return the callback function that needs to be called + foreach (self::$available_command_options as $option) { + $short_option = $option['short_option'] ?? ''; + if (str_ends_with($short_option, ':')) { + $short_option = rtrim($short_option, ':'); + } + $long_option = $option['long_option'] ?? ''; + if (str_ends_with($long_option, ':')) { + $long_option = rtrim($long_option, ':'); + } + if ($short_option === $set_option || + $long_option === $set_option) { + return $option['functions'] ?? [$option['function']] ?? []; + } + } + return []; + } + + /** + * Calls a function using the best suited PHP method. + * + * @param string $function The name of the function to call, either as a string or an instance of Closure. + * @param mixed $args An array of arguments to pass to the called function. * * @return void */ - public static function show_timestamp() { - self::$show_timestamp_log = true; - } - - /** - * Set to foreground when started - */ - public static function enable_daemon_mode() { - self::$daemon_mode = true; - self::$show_timestamp_log = false; - } - - /** - * Set the configuration file location to use for a config object - */ - public static function set_config_file(string $file = '/etc/fusionpbx/config.conf') { - if (empty(self::$config_file)) { - self::$config_file = $file; + private static function call_function($function, $args) { + if ($function === 'exit') { + //check for exit + exit($args); + } elseif ($function instanceof Closure || function_exists($function)) { + //globally available function or closure + $function($args); + } else { + static::$function($args); } - self::$config = new config(self::$config_file); - } - - /** - * Appends the CLI option to the list given to the user as a command line argument. - * @param command_option $option - * @return int The index of the item added - */ - public static function append_command_option(command_option $option): int { - $index = count(self::$available_command_options); - self::$available_command_options[$index] = $option->to_array(); - return $index; - } - - /** - * Add a new command option to the available options list. - * - * @param string $short_option Short option (e.g., 'h' for '-h') - * @param string $long_option Long option (e.g., 'help' for '--help') - * @param string $description Brief description of the option - * @param string $short_description Short description of the short option (optional) - * @param string $long_description Long description of the long option (optional) - * @param mixed[] ...$callback Callback function(s) associated with this option - * - * @return int Index of the newly added command option in self::$available_command_options array - */ - public static function add_command_option(string $short_option, string $long_option, string $description, string $short_description = '', string $long_description = '', ...$callback): int { - //use the option as the description if not filled in - if (empty($short_description)) { - $short_description = '-' . $short_option; - if (str_ends_with($short_option, ':')) { - $short_description .= " "; - } - } - if (empty($long_description)) { - $long_description = '-' . $long_option; - if (str_ends_with($long_option, ':')) { - $long_description .= " "; - } - } - $index = count(self::$available_command_options); - self::$available_command_options[$index]['short_option'] = $short_option; - self::$available_command_options[$index]['long_option'] = $long_option; - self::$available_command_options[$index]['description'] = $description; - self::$available_command_options[$index]['short_description'] = $short_description; - self::$available_command_options[$index]['long_description'] = $long_description; - self::$available_command_options[$index]['functions'] = $callback; - return $index; - } - - /** - * Returns the filename of the PID file. - * - * The PID file is located in /var/run/fusionpbx/ and its name is based on the base file name. - * - * @return string The path to the PID file - */ - public static function get_pid_filename(): string { - return '/var/run/fusionpbx/' . self::base_file_name() . '.pid'; } /** @@ -852,47 +608,341 @@ abstract class service { } /** - * Creates a new instance of the service class, initializing it and returning the object. + * Creates the service directory to store the PID * - * @return self A new instance of the service class. + * @return void + * @throws Exception thrown when the service directory is unable to be created */ - public static function create(): self { - //can only start from command line - defined('STDIN') or die('Unauthorized'); - - //parse the cli options and store them statically - self::parse_service_command_options(); - - //fork process - if (self::$daemon_mode) { - //force launching in a separate process - if ($pid = pcntl_fork()) { - exit; - } - - if ($cid = pcntl_fork()) { - exit; + private function create_service_directory() { + //make sure the /var/run/fusionpbx directory exists + if (!file_exists('/var/run/fusionpbx')) { + $result = mkdir('/var/run/fusionpbx', 0777, true); + if (!$result) { + throw new Exception('Failed to create /var/run/fusionpbx'); } } - - //create the config object if not already created - if (self::$config === null) { - self::$config = new config(self::$config_file); - } - - //get the name of child object - $class = self::base_class_name(); - - //create the child object - $service = new $class(); - - //initialize the service - $service->init(); - - //return the initialized object - return $service; } + /** + * Create an operating system PID file removing any existing PID file + */ + private function create_service_pid() { + // Set the pid filename + $basename = basename(self::$pid_file, '.pid'); + $pid = getmypid(); + + // Remove the old pid file + if (file_exists(self::$pid_file)) { + if (is_writable(self::$pid_file)) { + unlink(self::$pid_file); + } else { + throw new RuntimeException("Unable to write to PID file " . self::$pid_file, 73); //Unix error code 73 - unable to write/create file + } + } + + // Show the details to the user + self::log("Starting up..."); + self::log("Mode : " . (self::$daemon_mode ? "Daemon" : "Foreground"), LOG_INFO); + self::log("Service : $basename", LOG_INFO); + self::log("Process ID: $pid", LOG_INFO); + self::log("PID File : " . self::$pid_file, LOG_INFO); + self::log("Log level : " . self::log_level_to_string(self::$log_level), LOG_INFO); + self::log("Timestamps: " . (self::$show_timestamp_log ? "Yes" : "No"), LOG_INFO); + + // Save the pid file + $success = file_put_contents(self::$pid_file, $pid); + if ($success === false) { + throw new RuntimeException("Failed writing to PID file " . self::$pid_file, 74); //Unix error code 74 - I/O error + } + } + + /** + * Registers signal handlers for handling various system signals. + * + * This method sets up event listeners for the following signals: + * - SIGUSR1 and SIGHUP: reload the service settings from the database + * - SIGUSR2 and SIGTERM: shut down the service + * + * @return void + */ + private function register_signal_handlers() { + // Allow the calls to be made while the main loop is running + pcntl_async_signals(true); + + // A signal listener to reload the service for any config changes in the database + pcntl_signal(SIGUSR1, [$this, 'reload_settings']); + pcntl_signal(SIGHUP, [$this, 'reload_settings']); + + // A signal listener to stop the service + pcntl_signal(SIGUSR2, [self::class, 'shutdown']); + pcntl_signal(SIGTERM, [self::class, 'shutdown']); + } + + /** + * Display version notice + */ + abstract protected static function display_version(): void; + + /** + * Parses the debug level to an integer and stores it in the class for syslog use + * + * @param string $debug_level Debug level with any of the Linux system log levels + * + * @return void + */ + protected static function set_debug_level(string $debug_level) { + // Map user input log level to syslog constant + switch ($debug_level) { + case '0': + case 'emergency': + self::$log_level = LOG_EMERG; // Hardware failures + break; + case '1': + case 'alert': + self::$log_level = LOG_ALERT; // Loss of network connection or a condition that should be corrected immediately + break; + case '2': + case 'critical': + self::$log_level = LOG_CRIT; // Condition like low disk space + break; + case '3': + case 'error': + self::$log_level = LOG_ERR; // Database query failure, file not found + break; + case '4': + case 'warning': + self::$log_level = LOG_WARNING; // Deprecated function usage, approaching resource limits + break; + case '5': + case 'notice': + self::$log_level = LOG_NOTICE; // Normal conditions + break; + case '6': + case 'info': + self::$log_level = LOG_INFO; // Informational + break; + case '7': + case 'debug': + self::$log_level = LOG_DEBUG; // Debugging + break; + default: + self::$log_level = LOG_NOTICE; // Default to NOTICE if invalid level + } + + // When we are using LOG_DEBUG there is a high chance we are logging to the console + // directly without systemctl so enable the timestamps by default + if (self::$log_level === LOG_DEBUG && !self::$daemon_mode) { + self::show_timestamp(); + } + } + + /** + * Enables timestamp logging. + * + * @return void + */ + public static function show_timestamp() { + self::$show_timestamp_log = true; + } + + /** + * Logs the current and peak memory usage of the application at the INFO level. + * + * @return void + */ + protected static function show_mem_usage() { + //current memory + $memory_usage = memory_get_usage(); + //peak memory + $memory_peak = memory_get_peak_usage(); + self::log('Current memory: ' . round($memory_usage / 1024) . " KB", LOG_INFO); + self::log('Peak memory: ' . round($memory_peak / 1024) . " KB", LOG_INFO); + } + + /** + * Displays a help message for the available command options. + */ + protected static function display_help_message(): void { + //get the classname of the child class + $class_name = self::base_class_name(); + + //get the widest options for proper alignment + $width_short = max(array_map(function ($arr) { + return strlen($arr['short_description'] ?? ''); + }, self::$available_command_options)); + $width_long = max(array_map(function ($arr) { + return strlen($arr['long_description'] ?? ''); + }, self::$available_command_options)); + + //display usage help using the class name of child + echo "Usage: php $class_name [options]\n"; + + //display the options aligned to the widest short and long options + echo "Options:\n"; + foreach (self::$available_command_options as $option) { + printf("%-{$width_short}s %-{$width_long}s %s\n", + $option['short_description'], + $option['long_description'], + $option['description'] + ); + } + } + + /** + * Returns only the name of the class without namespace + * + * @return string base class name + */ + protected static function base_class_name(): string { + $class_and_namespace = explode('\\', static::class); + return array_pop($class_and_namespace); + } + + // + // Options built-in to the base service class. These can be overridden with the child class + // or they can be extended using the array + // + + /** + * Method to start the child class internal loop + */ + abstract public function run(): int; + + /** + * Ensures the correct PID file is unlinked when this process terminates. + * + * This method is called automatically by PHP when an object's reference count reaches zero, + * or when it's explicitly destroyed using unset(). It checks if the process is still running + * and unlinks the corresponding PID file. Finally, it ensures that any open log connections + * are properly closed before the script exits. + * + * @return void + */ + public function __destruct() { + //ensure we unlink the correct PID file if needed + if (self::is_running()) { + unlink(self::$pid_file); + self::log("Initiating Shutdown...", LOG_NOTICE); + $this->running = false; + } + //this should remain the last statement to execute before exit + closelog(); + } + + /** + * Checks the file system for a pid file that matches the process ID from this running instance + * + * @return bool true if pid exists and false if not + */ + public static function is_running(): bool { + return posix_getpid() === self::get_service_pid(); + } + + /** + * Returns the process ID of the service if it exists and is running. + * + * @return int|false The process ID of the service, or false if it does not exist or is not running. + */ + protected static function get_service_pid() { + if (file_exists(static::get_pid_filename())) { + $pid = file_get_contents(static::get_pid_filename()); + if (function_exists('posix_getsid')) { + if (posix_getsid($pid) !== false) { + //return the pid for reloading configuration + return intval($pid); + } + } else { + if (file_exists('/proc/' . $pid)) { + //return the pid for reloading configuration + return intval($pid); + } + } + } + return false; + } + + /** + * Returns the filename of the PID file. + * + * The PID file is located in /var/run/fusionpbx/ and its name is based on the base file name. + * + * @return string The path to the PID file + */ + public static function get_pid_filename(): string { + return '/var/run/fusionpbx/' . self::base_file_name() . '.pid'; + } + + /** + * Returns a file safe class name with \ from namespaces converted to _ + * + * @return string file safe name + */ + protected static function base_file_name(): string { + return str_replace('\\', "_", static::class); + } + + /** + * Logs to the system log or console when running in foreground + * + * @param string $message Message to display in the system log or console when running in foreground + * @param int $level (Optional) Level to use for logging to the console or daemon. Default value is LOG_NOTICE + * + * @return void + */ + protected static function log(string $message, int $level = LOG_NOTICE) { + // Check if we need to show the message + if ($level <= self::$log_level) { + // When not in daemon mode we log to console directly + if (!self::$daemon_mode) { + $level_as_string = self::log_level_to_string($level); + if (!self::$show_timestamp_log) { + echo "[$level_as_string] $message\n"; + } else { + $time = date('Y-m-d H:i:s'); + echo "[$time][$level_as_string] $message\n"; + } + } else { + // Log the message to syslog + syslog($level, 'fusionpbx[' . posix_getpid() . ']: [' . static::class . '] ' . $message); + } + } + } + + /** + * Converts a log level integer to its corresponding string representation. + * + * @param int $level The log level as an integer, defaults to LOG_NOTICE (5). + * + * @return string The log level as a string. + */ + private static function log_level_to_string(int $level = LOG_NOTICE): string { + switch ($level) { + case 0: + return 'EMERGENCY'; + case 1: + return 'ALERT'; + case 2: + return 'CRITICAL'; + case 3: + return 'ERROR'; + case 4: + return 'WARNING'; + case 5: + return 'NOTICE'; + case 6: + return 'INFO'; + case 7: + return 'DEBUG'; + default: + return 'INFO'; + } + } + + /** + * Child classes must provide a mechanism to reload settings + */ + abstract protected function reload_settings(): void; + /** * Logs a message at the DEBUG level. * @@ -917,7 +967,7 @@ abstract class service { /** * Logs a message at the NOTICE level. - * + * * @param string $message The message to be logged. Defaults to an empty string. * * @return void diff --git a/resources/classes/settings.php b/resources/classes/settings.php index 7c29d41b67..c45373f789 100644 --- a/resources/classes/settings.php +++ b/resources/classes/settings.php @@ -3,8 +3,8 @@ /** * The settings class is used to load settings using hierarchical overriding * - * The settings are loaded from the database tables default_settings, domain_settings, and user_settings in that order with - * Each setting overrides the setting from the previous table. + * The settings are loaded from the database tables default_settings, domain_settings, and user_settings in that order + * with Each setting overrides the setting from the previous table. * * @access public * @author Mark Crane @@ -12,49 +12,59 @@ class settings implements clear_cache { /** - * Set in the constructor. String used to load a specific domain. Must be a value domain UUID before sending to the constructor. + * Set in the constructor. String used to load a specific domain. Must be a value domain UUID before sending to the + * constructor. + * * @var string */ private $domain_uuid; /** - * Set in the constructor. String used to load a specific user. Must be a valid user UUID before sending to the constructor. + * Set in the constructor. String used to load a specific user. Must be a valid user UUID before sending to the + * constructor. + * * @var string */ private $user_uuid; /** * Set in the constructor. String used for loading a specific device UUID + * * @var string */ private $device_uuid; /** * Set in the constructor. String used for loading device profile + * * @var string */ private $device_profile_uuid; /** * Set in the constructor. Current category set to load + * * @var string */ private $category; /** * Internal array structure that is populated from the database + * * @var array Array of settings loaded from Default Settings */ private $settings; /** * Set in the constructor. Must be a database object and cannot be null. + * * @var database Database Object */ private $database; /** * Tracks if the APCu extension is loaded for database queries + * * @var bool */ private $apcu_enabled; @@ -63,9 +73,11 @@ class settings implements clear_cache { * Create a settings object using key/value pairs in the $setting_array. * * Valid values are: database, domain_uuid, user_uuid, device_uuid, device_profile_uuid, and category. + * * @param array setting_array + * * @depends database::new() - * @access public + * @access public */ public function __construct($setting_array = []) { @@ -89,7 +101,7 @@ class settings implements clear_cache { //trap passing a PDO object instead of the required database object if (!($this->database instanceof database)) { //should never happen but will trap it here just-in-case - throw new \InvalidArgumentException("Database object passed in settings class constructor is not a valid database object"); + throw new InvalidArgumentException("Database object passed in settings class constructor is not a valid database object"); } //set the values from the array @@ -102,14 +114,6 @@ class settings implements clear_cache { $this->reload(); } - /** - * Returns the database object used in the settings - * @return database Object - */ - public function database(): database { - return $this->database; - } - /** * Reloads the settings from the database */ @@ -144,107 +148,9 @@ class settings implements clear_cache { } } - /** - * Get the value utilizing the hierarchical overriding technique - * @param string|null $category Returns all settings when empty or the default value if the settings array is null - * @param string|null $subcategory Returns the array of category items when empty or the default value if the category array is null - * @param mixed $default_value allows default value returned if category and subcategory not found - */ - public function get(?string $category = null, ?string $subcategory = null, $default_value = null) { - - //incremental refinement from all settings to a single setting - if (empty($category)) { - return $this->settings ?? $default_value; - } - elseif (empty($subcategory)) { - return $this->settings[$category] ?? $default_value; - } - else { - return $this->settings[$category][$subcategory] ?? $default_value; - } - } - - /** - * Returns the domain_uuid in this object used to load the settings - * @return string UUID of the domain used to load the object or an empty string - */ - public function get_domain_uuid(): string { - if (!empty($this->domain_uuid)) { - return $this->domain_uuid; - } - return ""; - } - - /** - * Returns the user_uuid in this object used to load the settings - * @return string UUID of the user used to load the object or an empty string - */ - public function get_user_uuid(): string { - if (!empty($this->user_uuid)) { - return $this->user_uuid; - } - return ""; - } - - /** - * set the default, domain, user, device or device profile settings - * @param string $table_prefix prefix for the table. - * @param string $uuid uuid of the setting if available. If set to an empty string then a new uuid will be created. - * @param string $category Category of the setting. - * @param string $subcategory Subcategory of the setting. - * @param string $value (optional) Value to set. Default is empty string. - * @param string $type Type of the setting (array, numeric, text, etc) - * @param bool $enabled (optional) True or False. Default is True. - * @param string $description (optional) Description. Default is empty string. - */ - public function set(string $table_prefix, string $uuid, string $category, string $subcategory, string $value = "", string $type = 'text', bool $enabled = true, string $description = "") { - - //set the table name - $table_name = $table_prefix.'_settings'; - - //init record as an array - $record = []; - if(!empty($this->domain_uuid)) { - $record[$table_name][0]['domain_uuid'] = $this->domain_uuid; - } - if(!empty($this->user_uuid)) { - $record[$table_name][0]['user_uuid'] = $this->user_uuid; - } - if(!empty($this->device_uuid)) { - $record[$table_name][0]['device_uuid'] = $this->device_uuid; - } - if(!empty($this->device_profile_uuid)) { - $record[$table_name][0]['device_profile_uuid'] = $this->device_profile_uuid; - } - if(!is_uuid($uuid)) { - $uuid = uuid(); - } - - //build the array - $record[$table_name][0][$table_prefix.'_setting_uuid' ] = $uuid; - $record[$table_name][0][$table_prefix.'_setting_category' ] = $category; - $record[$table_name][0][$table_prefix.'_setting_subcategory'] = $subcategory; - $record[$table_name][0][$table_prefix.'_setting_name' ] = $type; - $record[$table_name][0][$table_prefix.'_setting_value' ] = $value; - $record[$table_name][0][$table_prefix.'_setting_enabled' ] = $enabled; - $record[$table_name][0][$table_prefix.'_setting_description'] = $description; - - //grant temporary permissions - $p = permissions::new(); - $p->add($table_prefix.'_setting_add', 'temp'); - $p->add($table_prefix.'_setting_edit', 'temp'); - - //execute insert - $this->database->app_name = $table_name; - $this->database->save($record); - - //revoke temporary permissions - $p->delete($table_prefix.'_setting_add', 'temp'); - $p->delete($table_prefix.'_setting_edit', 'temp'); - } - /** * Update the internal settings array with the default settings from the database + * * @access private */ private function default_settings() { @@ -275,14 +181,12 @@ class settings implements clear_cache { if (isset($row['default_setting_value']) && $row['default_setting_value'] !== '') { if ($name == "boolean") { $this->settings[$category][$subcategory] = filter_var($row['default_setting_value'], FILTER_VALIDATE_BOOLEAN); - } - elseif ($name == "array") { + } elseif ($name == "array") { if (!isset($this->settings[$category][$subcategory]) || !is_array($this->settings[$category][$subcategory])) { - $this->settings[$category][$subcategory] = array(); + $this->settings[$category][$subcategory] = []; } $this->settings[$category][$subcategory][] = $row['default_setting_value']; - } - else { + } else { $this->settings[$category][$subcategory] = $row['default_setting_value']; } } @@ -297,6 +201,7 @@ class settings implements clear_cache { /** * Update the internal settings array with the domain settings from the database + * * @access private */ private function domain_settings($domain_uuid = '', $i = 0) { @@ -312,7 +217,7 @@ class settings implements clear_cache { $this->domain_settings($uuid, $i++); } - $key = 'settings_domain_'.$domain_uuid; + $key = 'settings_domain_' . $domain_uuid; $result = ''; //if the apcu extension is loaded get the cached database result @@ -336,7 +241,7 @@ class settings implements clear_cache { $category = $row['domain_setting_category']; $subcategory = $row['domain_setting_subcategory']; if ($name == "array") { - $this->settings[$category][$subcategory] = array(); + $this->settings[$category][$subcategory] = []; } } @@ -351,11 +256,10 @@ class settings implements clear_cache { } if ($name == "array") { if (!isset($this->settings[$category][$subcategory]) || !is_array($this->settings[$category][$subcategory])) { - $this->settings[$category][$subcategory] = array(); + $this->settings[$category][$subcategory] = []; } $this->settings[$category][$subcategory][] = $row['domain_setting_value']; - } - else { + } else { $this->settings[$category][$subcategory] = $row['domain_setting_value']; } } @@ -366,11 +270,12 @@ class settings implements clear_cache { /** * Update the internal settings array with the user settings from the database - * @access private + * + * @access private * @depends $this->domain_uuid */ private function user_settings() { - $key = 'settings_user_'.$this->user_uuid; + $key = 'settings_user_' . $this->user_uuid; $result = ''; //if the apcu extension is loaded get the cached database result if ($this->apcu_enabled && apcu_exists($key)) { @@ -397,11 +302,9 @@ class settings implements clear_cache { if (isset($row['user_setting_value']) && $row['user_setting_value'] !== '') { if ($name == "boolean") { $this->settings[$category][$subcategory] = filter_var($row['user_setting_value'], FILTER_VALIDATE_BOOLEAN); - } - elseif ($name == "array") { + } elseif ($name == "array") { $this->settings[$category][$subcategory][] = $row['user_setting_value']; - } - else { + } else { $this->settings[$category][$subcategory] = $row['user_setting_value']; } @@ -413,7 +316,8 @@ class settings implements clear_cache { /** * Update the internal settings array with the device profile settings from the database - * @access private + * + * @access private * @depends $this->domain_uuid */ private function device_profile_settings() { @@ -438,7 +342,8 @@ class settings implements clear_cache { /** * Update the internal settings array with the device settings from the database - * @access private + * + * @access private * @depends $this->domain_uuid */ private function device_settings() { @@ -461,6 +366,15 @@ class settings implements clear_cache { } } + /** + * Clears the APC cache and reloads the settings object. + * + * This method checks if APC is enabled on the server and if so, it clears + * any cached entries that start with "settings_". It then recreates the + * settings object to load all settings from the database. + * + * @return void + */ public static function clear_cache() { if (function_exists('apcu_enabled') && apcu_enabled()) { $cache = apcu_cache_info(false); @@ -484,4 +398,126 @@ class settings implements clear_cache { } } } + + /** + * Returns the database object used in the settings + * + * @return database Object + */ + public function database(): database { + return $this->database; + } + + /** + * Returns the domain_uuid in this object used to load the settings + * + * @return string UUID of the domain used to load the object or an empty string + */ + public function get_domain_uuid(): string { + if (!empty($this->domain_uuid)) { + return $this->domain_uuid; + } + return ""; + } + + /** + * Returns the user_uuid in this object used to load the settings + * + * @return string UUID of the user used to load the object or an empty string + */ + public function get_user_uuid(): string { + if (!empty($this->user_uuid)) { + return $this->user_uuid; + } + return ""; + } + + /** + * Get the value utilizing the hierarchical overriding technique + * + * @param string|null $category Returns all settings when empty or the default value if the settings array is + * null + * @param string|null $subcategory Returns the array of category items when empty or the default value if the + * category array is null + * @param mixed $default_value allows default value returned if category and subcategory not found + */ + public function get(?string $category = null, ?string $subcategory = null, $default_value = null) { + + //incremental refinement from all settings to a single setting + if (empty($category)) { + return $this->settings ?? $default_value; + } elseif (empty($subcategory)) { + return $this->settings[$category] ?? $default_value; + } else { + return $this->settings[$category][$subcategory] ?? $default_value; + } + } + + /** + * Create or update an in-memory setting for a domain, user, device, + * or device profile. + * + * This method assembles a settings record, generates a UUID when needed, + * assigns ownership context (domain, user, device, or device profile), + * grants temporary permissions, and commits the record using the database + * abstraction layer. Only in-memory values are modified; this does not + * reload or apply settings system-wide. + * + * @param string $table_prefix Prefix used to build the settings table name. + * @param string $uuid UUID of the setting. A new UUID is generated + * if the provided value is empty or invalid. + * @param string $category Setting category. + * @param string $subcategory Setting subcategory. + * @param string $value Optional value for the setting. Defaults to an empty string. + * @param string $type Setting type (e.g. text, numeric, array). + * @param bool $enabled Whether the setting is enabled. Defaults to true. + * @param string $description Optional description for the setting. + * + * @return void + */ + public function set(string $table_prefix, string $uuid, string $category, string $subcategory, string $value = "", string $type = 'text', bool $enabled = true, string $description = "") { + + //set the table name + $table_name = $table_prefix . '_settings'; + + //init record as an array + $record = []; + if (!empty($this->domain_uuid)) { + $record[$table_name][0]['domain_uuid'] = $this->domain_uuid; + } + if (!empty($this->user_uuid)) { + $record[$table_name][0]['user_uuid'] = $this->user_uuid; + } + if (!empty($this->device_uuid)) { + $record[$table_name][0]['device_uuid'] = $this->device_uuid; + } + if (!empty($this->device_profile_uuid)) { + $record[$table_name][0]['device_profile_uuid'] = $this->device_profile_uuid; + } + if (!is_uuid($uuid)) { + $uuid = uuid(); + } + + //build the array + $record[$table_name][0][$table_prefix . '_setting_uuid'] = $uuid; + $record[$table_name][0][$table_prefix . '_setting_category'] = $category; + $record[$table_name][0][$table_prefix . '_setting_subcategory'] = $subcategory; + $record[$table_name][0][$table_prefix . '_setting_name'] = $type; + $record[$table_name][0][$table_prefix . '_setting_value'] = $value; + $record[$table_name][0][$table_prefix . '_setting_enabled'] = $enabled; + $record[$table_name][0][$table_prefix . '_setting_description'] = $description; + + //grant temporary permissions + $p = permissions::new(); + $p->add($table_prefix . '_setting_add', 'temp'); + $p->add($table_prefix . '_setting_edit', 'temp'); + + //execute insert + $this->database->app_name = $table_name; + $this->database->save($record); + + //revoke temporary permissions + $p->delete($table_prefix . '_setting_add', 'temp'); + $p->delete($table_prefix . '_setting_edit', 'temp'); + } } diff --git a/resources/classes/sounds.php b/resources/classes/sounds.php index fc654a03a8..fce92e31c2 100644 --- a/resources/classes/sounds.php +++ b/resources/classes/sounds.php @@ -2,31 +2,36 @@ /** * sounds class - * - * @method string get */ class sounds { /** - * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array + * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set + * in the session global array + * * @var string */ public $domain_uuid; /** - * Additional public variables - */ + * Additional public variables + */ public $sound_types; public $full_path; /** * Set in the constructor. Must be a database object and cannot be null. + * * @var database Database Object */ private $database; /** - * Called when the object is created + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $setting_array An optional array of settings to override default values. Defaults to []. */ public function __construct(array $setting_array = []) { //set domain and user UUIDs @@ -37,76 +42,75 @@ class sounds { } /** - * Add a specific item in the cache - * @var array $array - * @var string $value string to be cached + * Retrieves a list of sound files based on the specified types. + * + * @return array A multidimensional array containing the sound files, where each sub-array has two keys: 'name' and + * 'value'. */ public function get() { //miscellaneous - if (empty($this->sound_types) || (is_array($this->sound_types) && in_array('miscellaneous', $this->sound_types))) { - $x = 0; - if (if_group("superadmin")) { - $array['miscellaneous'][$x]['name'] = "say"; - $array['miscellaneous'][$x]['value'] = "say:"; - $x++; - $array['miscellaneous'][$x]['name'] = "tone_stream"; - $array['miscellaneous'][$x]['value'] = "tone_stream:"; - } + if (empty($this->sound_types) || (is_array($this->sound_types) && in_array('miscellaneous', $this->sound_types))) { + $x = 0; + if (if_group("superadmin")) { + $array['miscellaneous'][$x]['name'] = "say"; + $array['miscellaneous'][$x]['value'] = "say:"; + $x++; + $array['miscellaneous'][$x]['name'] = "tone_stream"; + $array['miscellaneous'][$x]['value'] = "tone_stream:"; } + } //recordings - if ((empty($this->sound_types) || (is_array($this->sound_types) && in_array('recordings', $this->sound_types))) && file_exists($_SERVER["PROJECT_ROOT"]."/app/recordings/app_config.php")) { - $sql = "select recording_name, recording_filename from v_recordings "; - $sql .= "where domain_uuid = :domain_uuid "; - $sql .= "order by recording_name asc "; - $parameters['domain_uuid'] = $_SESSION["domain_uuid"]; - $recordings = $this->database->select($sql, $parameters, 'all'); - if (is_array($recordings) && @sizeof($recordings) != 0) { - foreach ($recordings as $x => $row) { - $recording_name = $row["recording_name"]; - $recording_filename = $row["recording_filename"]; - $recording_path = !empty($this->full_path) && is_array($this->full_path) && in_array('recordings', $this->full_path) ? $_SESSION['switch']['recordings']['dir'].'/'.$_SESSION['domain_name'].'/' : null; - $array['recordings'][$x]['name'] = $recording_name; - $array['recordings'][$x]['value'] = $recording_path.$recording_filename; - } + if ((empty($this->sound_types) || (is_array($this->sound_types) && in_array('recordings', $this->sound_types))) && file_exists($_SERVER["PROJECT_ROOT"] . "/app/recordings/app_config.php")) { + $sql = "select recording_name, recording_filename from v_recordings "; + $sql .= "where domain_uuid = :domain_uuid "; + $sql .= "order by recording_name asc "; + $parameters['domain_uuid'] = $_SESSION["domain_uuid"]; + $recordings = $this->database->select($sql, $parameters, 'all'); + if (is_array($recordings) && @sizeof($recordings) != 0) { + foreach ($recordings as $x => $row) { + $recording_name = $row["recording_name"]; + $recording_filename = $row["recording_filename"]; + $recording_path = !empty($this->full_path) && is_array($this->full_path) && in_array('recordings', $this->full_path) ? $_SESSION['switch']['recordings']['dir'] . '/' . $_SESSION['domain_name'] . '/' : null; + $array['recordings'][$x]['name'] = $recording_name; + $array['recordings'][$x]['value'] = $recording_path . $recording_filename; } - unset($sql, $parameters, $recordings, $row); } + unset($sql, $parameters, $recordings, $row); + } //phrases - if ((empty($this->sound_types) || (is_array($this->sound_types) && in_array('phrases', $this->sound_types))) && file_exists($_SERVER["PROJECT_ROOT"]."/app/phrases/app_config.php")) { - $sql = "select * from v_phrases "; - $sql .= "where domain_uuid = :domain_uuid "; - $parameters['domain_uuid'] = $_SESSION["domain_uuid"]; - $phrases = $this->database->select($sql, $parameters, 'all'); - if (is_array($phrases) && @sizeof($phrases) != 0) { - foreach ($phrases as $row) { - $array['phrases'][$x]['name'] = "phrase:".$row["phrase_name"]; - $array['phrases'][$x]['value'] = "phrase:".$row["phrase_uuid"]; - $x++; - } + if ((empty($this->sound_types) || (is_array($this->sound_types) && in_array('phrases', $this->sound_types))) && file_exists($_SERVER["PROJECT_ROOT"] . "/app/phrases/app_config.php")) { + $sql = "select * from v_phrases "; + $sql .= "where domain_uuid = :domain_uuid "; + $parameters['domain_uuid'] = $_SESSION["domain_uuid"]; + $phrases = $this->database->select($sql, $parameters, 'all'); + if (is_array($phrases) && @sizeof($phrases) != 0) { + foreach ($phrases as $row) { + $array['phrases'][$x]['name'] = "phrase:" . $row["phrase_name"]; + $array['phrases'][$x]['value'] = "phrase:" . $row["phrase_uuid"]; + $x++; } - unset($sql, $parameters, $phrases, $row); } + unset($sql, $parameters, $phrases, $row); + } //sounds - if ((empty($this->sound_types) || (is_array($this->sound_types) && in_array('sounds', $this->sound_types))) && file_exists($_SERVER["PROJECT_ROOT"]."/app/phrases/app_config.php")) { - $file = new file; - $sound_files = $file->sounds(); - if (is_array($sound_files) && @sizeof($sound_files) != 0) { - foreach ($sound_files as $value) { - if (substr($value, 0, 71) == "\$\${sounds_dir}/\${default_language}/\${default_dialect}/\${default_voice}/") { - $value = substr($value, 71); - } - $array['sounds'][$x]['name'] = $value; - $array['sounds'][$x]['value'] = $value; - $x++; + if ((empty($this->sound_types) || (is_array($this->sound_types) && in_array('sounds', $this->sound_types))) && file_exists($_SERVER["PROJECT_ROOT"] . "/app/phrases/app_config.php")) { + $file = new file; + $sound_files = $file->sounds(); + if (is_array($sound_files) && @sizeof($sound_files) != 0) { + foreach ($sound_files as $value) { + if (substr($value, 0, 71) == "\$\${sounds_dir}/\${default_language}/\${default_dialect}/\${default_voice}/") { + $value = substr($value, 71); } + $array['sounds'][$x]['name'] = $value; + $array['sounds'][$x]['value'] = $value; + $x++; } } + } //send the results - return $array; + return $array; } } - -?> diff --git a/resources/classes/switch_settings.php b/resources/classes/switch_settings.php index 0ff40a9435..fb01fc21dd 100644 --- a/resources/classes/switch_settings.php +++ b/resources/classes/switch_settings.php @@ -5,291 +5,310 @@ * * @method settings will add missing switch directories to default settings */ - class switch_settings { +class switch_settings { - public $event_socket_ip_address; - public $event_socket_port; - public $event_socket_password; + public $event_socket_ip_address; + public $event_socket_port; + public $event_socket_password; - /** - * Set in the constructor. Must be a database object and cannot be null. - * @var database Database Object - */ - private $database; + /** + * Set in the constructor. Must be a database object and cannot be null. + * + * @var database Database Object + */ + private $database; - /** - * Settings object set in the constructor. Must be a settings object and cannot be null. - * @var settings Settings Object - */ - private $settings; + /** + * Settings object set in the constructor. Must be a settings object and cannot be null. + * + * @var settings Settings Object + */ + private $settings; - /** - * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array - * @var string - */ - private $user_uuid; + /** + * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in + * the session global array + * + * @var string + */ + private $user_uuid; - /** - * Username set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array - * @var string - */ - private $username; + /** + * Username set in the constructor. This can be passed in through the $settings_array associative array or set in + * the session global array + * + * @var string + */ + private $username; - /** - * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array - * @var string - */ - private $domain_uuid; + /** + * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set + * in the session global array + * + * @var string + */ + private $domain_uuid; - /** - * Domain name set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array - * @var string - */ - private $domain_name; + /** + * Domain name set in the constructor. This can be passed in through the $settings_array associative array or set + * in the session global array + * + * @var string + */ + private $domain_name; - /** - * called when the object is created - */ - public function __construct(array $setting_array = []) { - //set domain and user UUIDs - $this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? ''; - $this->domain_name = $setting_array['domain_name'] ?? $_SESSION['domain_name'] ?? ''; - $this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? ''; - $this->username = $setting_array['username'] ?? $_SESSION['username'] ?? ''; + /** + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $setting_array An optional array of settings to override default values. Defaults to []. + */ + public function __construct(array $setting_array = []) { + //set domain and user UUIDs + $this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? ''; + $this->domain_name = $setting_array['domain_name'] ?? $_SESSION['domain_name'] ?? ''; + $this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? ''; + $this->username = $setting_array['username'] ?? $_SESSION['username'] ?? ''; - //set objects - $this->database = $setting_array['database'] ?? database::new(); - $this->settings = $setting_array['settings'] ?? new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]); + //set objects + $this->database = $setting_array['database'] ?? database::new(); + $this->settings = $setting_array['settings'] ?? new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]); - } - - /** - * settings Set switch directories in default settings - */ - public function settings() { - - //connect to event socket - $esl = event_socket::create($this->event_socket_ip_address, $this->event_socket_port, $this->event_socket_password); - - //run the api command - $result = $esl->request('api global_getvar'); - - //set the result as a named array - $vars = array(); - foreach (explode("\n", $result) as $row) { - $a = explode("=", $row); - if (substr($a[0], -4) == "_dir") { - $vars[$a[0]] = $a[1]; - } - } - - //set defaults - $vars['base_dir'] = $vars['base_dir'] ?? ''; - $vars['conf_dir'] = $vars['conf_dir'] ?? ''; - $vars['db_dir'] = $vars['db_dir'] ?? ''; - $vars['recordings_dir'] = $vars['recordings_dir'] ?? ''; - $vars['script_dir'] = $vars['script_dir'] ?? ''; - $vars['sounds_dir'] = $vars['sounds_dir'] ?? ''; - $vars['storage_dir'] = $vars['storage_dir'] ?? ''; - $vars['grammar_dir'] = $vars['grammar_dir'] ?? ''; - $vars['log_dir'] = $vars['log_dir'] ?? ''; - $vars['mod_dir'] = $vars['mod_dir'] ?? ''; - - //set the bin directory - if ($vars['base_dir'] == "/usr/local/freeswitch") { - $bin = '/usr/local/freeswitch/bin'; - } - else { - $bin = ''; - } - - //create the default settings array - $x=0; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'bin'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $bin; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'base'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['base_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'call_center'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['conf_dir'].'/autoload_configs'; - $array[$x]['default_setting_enabled'] = 'false'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'conf'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['conf_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'db'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['db_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'dialplan'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['conf_dir'].'/dialplan'; - $array[$x]['default_setting_enabled'] = 'false'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'extensions'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['conf_dir'].'/directory'; - $array[$x]['default_setting_enabled'] = 'false'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'grammar'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['grammar_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'log'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['log_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'mod'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['mod_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'languages'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['conf_dir'].'/languages'; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'recordings'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['recordings_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'scripts'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['script_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'sip_profiles'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['conf_dir'].'/sip_profiles'; - $array[$x]['default_setting_enabled'] = 'false'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'sounds'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['sounds_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'storage'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['storage_dir']; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - $array[$x]['default_setting_category'] = 'switch'; - $array[$x]['default_setting_subcategory'] = 'voicemail'; - $array[$x]['default_setting_name'] = 'dir'; - $array[$x]['default_setting_value'] = $vars['storage_dir'].'/voicemail'; - $array[$x]['default_setting_enabled'] = 'true'; - $array[$x]['default_setting_description'] = ''; - $x++; - - //get an array of the default settings - $sql = "select * from v_default_settings "; - $sql .= "where default_setting_category = 'switch' "; - $default_settings = $this->database->select($sql, null, 'all'); - unset($sql); - - //find the missing default settings - $x = 0; - foreach ($array as $setting) { - $found = false; - $missing[$x] = $setting; - foreach ($default_settings as $row) { - if (trim($row['default_setting_subcategory']) == trim($setting['default_setting_subcategory'])) { - $found = true; - //remove items from the array that were found - unset($missing[$x]); - } - } - $x++; - } - unset($array); - - //add the missing default settings - if (count($missing) > 0) { - $i = 1; - foreach ($missing as $row) { - //build insert array - $array['default_settings'][$i]['default_setting_uuid'] = uuid(); - $array['default_settings'][$i]['default_setting_category'] = $row['default_setting_category']; - $array['default_settings'][$i]['default_setting_subcategory'] = $row['default_setting_subcategory']; - $array['default_settings'][$i]['default_setting_name'] = $row['default_setting_name']; - $array['default_settings'][$i]['default_setting_value'] = $row['default_setting_value']; - $array['default_settings'][$i]['default_setting_enabled'] = $row['default_setting_enabled']; - $array['default_settings'][$i]['default_setting_description'] = $row['default_setting_description']; - - //increment the row id - $i++; - } - if (is_array($array) && @sizeof($array) != 0) { - //grant temporary permissions - $p = permissions::new(); - $p->add('default_setting_add', 'temp'); - - //execute insert - $this->database->save($array); - - //clear the apcu cache - settings::clear_cache(); - - //revoke temporary permissions - $p->delete('default_setting_add', 'temp'); - } - unset($missing); - } - - //set the default settings - if (!empty($array) && is_array($array)) { - foreach ($array as $row) { - if (isset($row['default_setting_enabled']) && $row['default_setting_enabled'] == "true" && isset($row['default_setting_subcategory'])) { - $_SESSION['switch'][$row['default_setting_subcategory']][$row['default_setting_name']] = $row['default_setting_value'] ?? ''; - } - } - } - - //unset the array variable - unset($array); - } } + + /** + * Get the default settings for the application. + * + * This method retrieves the default settings from the event socket and + * creates an array of default setting categories, subcategories, names, + * values, and descriptions. + * + * @return array An array of default settings. + */ + public function settings() { + + //connect to event socket + $esl = event_socket::create($this->event_socket_ip_address, $this->event_socket_port, $this->event_socket_password); + + //run the api command + $result = $esl->request('api global_getvar'); + + //set the result as a named array + $vars = []; + foreach (explode("\n", $result) as $row) { + $a = explode("=", $row); + if (substr($a[0], -4) == "_dir") { + $vars[$a[0]] = $a[1]; + } + } + + //set defaults + $vars['base_dir'] = $vars['base_dir'] ?? ''; + $vars['conf_dir'] = $vars['conf_dir'] ?? ''; + $vars['db_dir'] = $vars['db_dir'] ?? ''; + $vars['recordings_dir'] = $vars['recordings_dir'] ?? ''; + $vars['script_dir'] = $vars['script_dir'] ?? ''; + $vars['sounds_dir'] = $vars['sounds_dir'] ?? ''; + $vars['storage_dir'] = $vars['storage_dir'] ?? ''; + $vars['grammar_dir'] = $vars['grammar_dir'] ?? ''; + $vars['log_dir'] = $vars['log_dir'] ?? ''; + $vars['mod_dir'] = $vars['mod_dir'] ?? ''; + + //set the bin directory + if ($vars['base_dir'] == "/usr/local/freeswitch") { + $bin = '/usr/local/freeswitch/bin'; + } else { + $bin = ''; + } + + //create the default settings array + $x = 0; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'bin'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $bin; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'base'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['base_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'call_center'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['conf_dir'] . '/autoload_configs'; + $array[$x]['default_setting_enabled'] = 'false'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'conf'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['conf_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'db'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['db_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'dialplan'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['conf_dir'] . '/dialplan'; + $array[$x]['default_setting_enabled'] = 'false'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'extensions'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['conf_dir'] . '/directory'; + $array[$x]['default_setting_enabled'] = 'false'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'grammar'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['grammar_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'log'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['log_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'mod'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['mod_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'languages'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['conf_dir'] . '/languages'; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'recordings'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['recordings_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'scripts'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['script_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'sip_profiles'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['conf_dir'] . '/sip_profiles'; + $array[$x]['default_setting_enabled'] = 'false'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'sounds'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['sounds_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'storage'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['storage_dir']; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + $array[$x]['default_setting_category'] = 'switch'; + $array[$x]['default_setting_subcategory'] = 'voicemail'; + $array[$x]['default_setting_name'] = 'dir'; + $array[$x]['default_setting_value'] = $vars['storage_dir'] . '/voicemail'; + $array[$x]['default_setting_enabled'] = 'true'; + $array[$x]['default_setting_description'] = ''; + $x++; + + //get an array of the default settings + $sql = "select * from v_default_settings "; + $sql .= "where default_setting_category = 'switch' "; + $default_settings = $this->database->select($sql, null, 'all'); + unset($sql); + + //find the missing default settings + $x = 0; + foreach ($array as $setting) { + $found = false; + $missing[$x] = $setting; + foreach ($default_settings as $row) { + if (trim($row['default_setting_subcategory']) == trim($setting['default_setting_subcategory'])) { + $found = true; + //remove items from the array that were found + unset($missing[$x]); + } + } + $x++; + } + unset($array); + + //add the missing default settings + if (count($missing) > 0) { + $i = 1; + foreach ($missing as $row) { + //build insert array + $array['default_settings'][$i]['default_setting_uuid'] = uuid(); + $array['default_settings'][$i]['default_setting_category'] = $row['default_setting_category']; + $array['default_settings'][$i]['default_setting_subcategory'] = $row['default_setting_subcategory']; + $array['default_settings'][$i]['default_setting_name'] = $row['default_setting_name']; + $array['default_settings'][$i]['default_setting_value'] = $row['default_setting_value']; + $array['default_settings'][$i]['default_setting_enabled'] = $row['default_setting_enabled']; + $array['default_settings'][$i]['default_setting_description'] = $row['default_setting_description']; + + //increment the row id + $i++; + } + if (is_array($array) && @sizeof($array) != 0) { + //grant temporary permissions + $p = permissions::new(); + $p->add('default_setting_add', 'temp'); + + //execute insert + $this->database->save($array); + + //clear the apcu cache + settings::clear_cache(); + + //revoke temporary permissions + $p->delete('default_setting_add', 'temp'); + } + unset($missing); + } + + //set the default settings + if (!empty($array) && is_array($array)) { + foreach ($array as $row) { + if (isset($row['default_setting_enabled']) && $row['default_setting_enabled'] == "true" && isset($row['default_setting_subcategory'])) { + $_SESSION['switch'][$row['default_setting_subcategory']][$row['default_setting_name']] = $row['default_setting_value'] ?? ''; + } + } + } + + //unset the array variable + unset($array); + } +} diff --git a/resources/classes/template.php b/resources/classes/template.php index 3b3d2c57f4..db4e002d8a 100644 --- a/resources/classes/template.php +++ b/resources/classes/template.php @@ -25,67 +25,91 @@ */ //define the template class - class template { +class template { - public $engine; - public $template_dir; - public $cache_dir; - private $object; - private $var_array; + public $engine; + public $template_dir; + public $cache_dir; + private $object; + private $var_array; - public function __construct(){ - } + public function __construct() { + } - public function init() { - if ($this->engine === 'smarty') { - require_once "resources/templates/engine/smarty/Smarty.class.php"; - $this->object = new Smarty(); - $this->object->setTemplateDir($this->template_dir); - $this->object->setCompileDir($this->cache_dir); - $this->object->setCacheDir($this->cache_dir); - $this->object->registerPlugin("modifier","in_array", "in_array"); - } - if ($this->engine === 'raintpl') { - require_once "resources/templates/engine/raintpl/rain.tpl.class.php"; - $this->object = new RainTPL(); - RainTPL::configure('tpl_dir', realpath($this->template_dir)."/"); - RainTPL::configure('cache_dir', realpath($this->cache_dir)."/"); - } - if ($this->engine === 'twig') { - require_once "resources/templates/engine/Twig/Autoloader.php"; - Twig_Autoloader::register(); - $loader = new Twig_Loader_Filesystem($this->template_dir); - $this->object = new Twig_Environment($loader); - $lexer = new Twig_Lexer($this->object, array( - 'tag_comment' => array('{*', '*}'), - 'tag_block' => array('{', '}'), - 'tag_variable' => array('{$', '}'), - )); - $this->object->setLexer($lexer); - } - } - - public function assign($key, $value) { - if ($this->engine === 'smarty') { - $this->object->assign($key, $value); - } - if ($this->engine === 'raintpl') { - $this->object->assign($key, $value); - } - if ($this->engine === 'twig') { - $this->var_array[$key] = $value; - } - } - - public function render($name) { - if ($this->engine === 'smarty') { - return $this->object->fetch($name); - } - if ($this->engine === 'raintpl') { - return $this->object-> draw($name, 'return_string=true'); - } - if ($this->engine === 'twig') { - return $this->object->render($name,$this->var_array); - } - } + /** + * Initializes the template engine based on the selected engine. + * + * @access public + */ + public function init() { + if ($this->engine === 'smarty') { + require_once "resources/templates/engine/smarty/Smarty.class.php"; + $this->object = new Smarty(); + $this->object->setTemplateDir($this->template_dir); + $this->object->setCompileDir($this->cache_dir); + $this->object->setCacheDir($this->cache_dir); + $this->object->registerPlugin("modifier", "in_array", "in_array"); } + if ($this->engine === 'raintpl') { + require_once "resources/templates/engine/raintpl/rain.tpl.class.php"; + $this->object = new RainTPL(); + RainTPL::configure('tpl_dir', realpath($this->template_dir) . "/"); + RainTPL::configure('cache_dir', realpath($this->cache_dir) . "/"); + } + if ($this->engine === 'twig') { + require_once "resources/templates/engine/Twig/Autoloader.php"; + Twig_Autoloader::register(); + $loader = new Twig_Loader_Filesystem($this->template_dir); + $this->object = new Twig_Environment($loader); + $lexer = new Twig_Lexer($this->object, [ + 'tag_comment' => ['{*', '*}'], + 'tag_block' => ['{', '}'], + 'tag_variable' => ['{$', '}'], + ]); + $this->object->setLexer($lexer); + } + } + + /** + * Assigns a value to the template engine based on the selected engine. + * + * @param string $key The key for the assigned value. + * @param mixed $value The value to be assigned. + * + * @access public + * + * @return void + */ + public function assign($key, $value) { + if ($this->engine === 'smarty') { + $this->object->assign($key, $value); + } + if ($this->engine === 'raintpl') { + $this->object->assign($key, $value); + } + if ($this->engine === 'twig') { + $this->var_array[$key] = $value; + } + } + + /** + * Renders the given template using the configured engine. + * + * @param string $name Name of the template to render. Values can be 'smarty', 'raintpl' or 'twig' (case sensitive) + * + * @return mixed The rendered template output, depending on the used engine + * + * @access public + */ + public function render($name) { + if ($this->engine === 'smarty') { + return $this->object->fetch($name); + } + if ($this->engine === 'raintpl') { + return $this->object->draw($name, 'return_string=true'); + } + if ($this->engine === 'twig') { + return $this->object->render($name, $this->var_array); + } + } +} diff --git a/resources/classes/text.php b/resources/classes/text.php index eda16994a1..bf2b6a7d67 100644 --- a/resources/classes/text.php +++ b/resources/classes/text.php @@ -6,17 +6,24 @@ */ class text implements clear_cache { + /** + * Translated flattened text array using the file path as key + * + * @var array + */ + private static $tanslated; /** * Contains the list of supported languages + * * @var array */ public $languages; - /** * Legacy older list of supported languages + * * @var array */ - public $legacy_map = array( + public $legacy_map = [ 'he' => 'he-il', 'pl' => 'pl-pl', 'uk' => 'uk-ua', @@ -30,439 +37,94 @@ class text implements clear_cache { 'es' => 'es-cl', 'fr' => 'fr-fr', 'pt' => 'pt-pt', - ); - + ]; /** * Set in the constructor. Must be a database object and cannot be null. + * * @var database Database Object */ private $database; - /** * Settings object set in the constructor. Must be a settings object and cannot be null. + * * @var settings Settings Object */ private $settings; - /** - * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array + * User UUID set in the constructor. This can be passed in through the $settings_array associative array or set in + * the session global array + * * @var string */ private $user_uuid; - /** - * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set in the session global array + * Domain UUID set in the constructor. This can be passed in through the $settings_array associative array or set + * in the session global array + * * @var string */ private $domain_uuid; - - /** - * Translated flattened text array using the file path as key - * @var array - */ - private static $tanslated; - /** * @var bool */ private $apcu_enabled; /** - * Called when the object is created + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $setting_array An optional array of settings to override default values. Defaults to []. */ public function __construct($setting_array = []) { //define the text array - $text = array(); + $text = []; //check for apcu caching - $this->apcu_enabled = function_exists('apcu_enabled') && apcu_enabled(); + $this->apcu_enabled = function_exists('apcu_enabled') && apcu_enabled(); //set the domain and user uuids - $this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? ''; - $this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? ''; + $this->domain_uuid = $setting_array['domain_uuid'] ?? $_SESSION['domain_uuid'] ?? ''; + $this->user_uuid = $setting_array['user_uuid'] ?? $_SESSION['user_uuid'] ?? ''; //open a database connection - if (empty($setting_array['database'])) { - $this->database = database::new(); - } else { - $this->database = $setting_array['database']; - } + if (empty($setting_array['database'])) { + $this->database = database::new(); + } else { + $this->database = $setting_array['database']; + } //load the settings - if (empty($setting_array['settings'])) { - $this->settings = new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]); - } else { - $this->settings = $setting_array['settings']; - } + if (empty($setting_array['settings'])) { + $this->settings = new settings(['database' => $this->database, 'domain_uuid' => $this->domain_uuid, 'user_uuid' => $this->user_uuid]); + } else { + $this->settings = $setting_array['settings']; + } //get the global app_languages.php so we can get the list of languages - if (file_exists($_SERVER["PROJECT_ROOT"]."/resources/app_languages.php")) { - include $_SERVER["PROJECT_ROOT"]."/resources/app_languages.php"; - } + if (file_exists($_SERVER["PROJECT_ROOT"] . "/resources/app_languages.php")) { + include $_SERVER["PROJECT_ROOT"] . "/resources/app_languages.php"; + } //get the list of languages, remove en-us, sort it then put en-us in front - unset($text['language-name']['en-us']); - if (is_array($text['language-name'])) { - $languages = array_keys($text['language-name']); - asort($languages); - array_unshift($languages, 'en-us'); - } - - //support legacy variable - if (is_array($languages)) { - $_SESSION['app']['languages'] = $languages; - $this->languages = $languages; - } - } - - /** - * Get a specific language from the language file - * @param string|null $language_code examples: en-us, es-cl, fr-fr, pt-pt - * @param string|null $app_path examples: app/exec or core/domains - * @param bool $exclude_global Exclude the global languages file - * - * @return array A flattened array containing the desired language - */ - public function get(?string $language_code = null, ?string $app_path = null, bool $exclude_global = false) { - - //define the text array - $text = []; - - //check the session language - if ($language_code == null) { - $language_code = $this->settings->get('domain', 'language', 'en-us'); - } - - //check the language code - if (strlen($language_code) == 2 && array_key_exists($language_code, $this->legacy_map)) { - $language_code = $this->legacy_map[$language_code]; - } - - //get the app_languages.php - if ($app_path != null) { - $lang_path = $_SERVER["PROJECT_ROOT"]."/".$app_path; - } - else { - $lang_path = getcwd(); - } - - //check the class cache - $cache_key = "text_{$language_code}_{$lang_path}"; - if ($this->apcu_enabled && apcu_exists($cache_key)) { - return apcu_fetch($cache_key); - - } - if (isset(self::$tanslated[$cache_key])) { - return self::$tanslated[$cache_key]; - } - - //get the global app_languages.php - if (!$exclude_global && file_exists($_SERVER["PROJECT_ROOT"]."/resources/app_languages.php")) { - require $_SERVER["PROJECT_ROOT"]."/resources/app_languages.php"; - } - - if (file_exists($lang_path."/app_languages.php") && ($lang_path != 'resources' or $exclude_global)) { - include "{$lang_path}/app_languages.php"; - } - //else { - // throw new Exception("could not find app_languages for '$app_path'"); - //} - - //reduce to specific language - if ($language_code != 'all' && is_array($text)) { - foreach ($text as $key => $value) { - if (isset($value[$language_code]) && !empty($value[$language_code])) { - //use the selected language - $text[$key] = $value[$language_code]; - } elseif (isset($value['en-us'])) { - //fallback to en-us - $text[$key] = $value['en-us']; - } else { - $text[$key] = ''; - } - } - } - - //cache the reduced language using the file as a key - if ($this->apcu_enabled) { - apcu_store($cache_key, $text); - } else { - self::$tanslated[$cache_key] = $text; - } - - //return the array of translations - return $text; - } - - /** - * reorganize an app_languages.php into a consistent format - * @var string $app_path examples: app/exec or core/domains - * @var string $no_sort don't sort the text label order - */ - public function organize_language($app_path = null, $no_sort = false) { - - //clear $text ready for the import - $text = array(); - - //get the app_languages.php - if ($app_path == null) { - throw new Exception("\$app_path must be specified"); - } - $lang_path = $_SERVER["PROJECT_ROOT"]."/$app_path/app_languages.php"; - if (!file_exists($lang_path)) { - throw new Exception("could not find app_languages for '$app_path'"); - } - require $lang_path; - - if (!is_array($text)) { - throw new Exception("failed to import text data from '$app_path'"); - } - - //collect existing comments - $comment = array(); - $file_handle = fopen($lang_path, "r"); - while (!feof($file_handle)) { - if(preg_match('/\$text\[[\'"](.+)[\'"]\]\[[\'"](.+)[\'"]]\s+=\s+[\'"].*[\'"];\s+\/\/(.+)/', fgets($file_handle), $matches)){ - $comment[$matches[0]][$matches[1]] = $matches[2]; - } - } - fclose($file_handle); - - //open the language file for writing - $lang_file = fopen($lang_path, 'w'); - date_default_timezone_set('UTC'); - fwrite($lang_file, "languages as $language) { - $temp_B["language-$language"] = $text["language-$language"]; - unset($text["language-$language"]); - } - $temp_C["language-en-us"] = $temp_B["language-en-us"]; - unset($temp_B["language-en-us"]); - ksort($temp_B); - $temp_B = array_merge($temp_C, $temp_B); - ksort($text); - $text = array_merge($temp_A, $temp_B, $text); - unset($temp_A, $temp_B, $temp_C); - } - else { - ksort($text); - } - } - else { - if ($app_path == 'resources') { - foreach($this->languages as $language) { - $label = array_shift($text["language-$language"]); - if (empty($label)) - $label = $language; - $text["language-$language"]['en-us'] = $label; - } - } - } - $last_lang_label = ""; - foreach ($text as $lang_label => $lang_codes) { - - //behave differently if we are one of the special language-* tags - if (preg_match('/\Alanguage-(\w{2}|\w{2}-\w{2})\z/', $lang_label, $lang_code)) { - if ($lang_label == 'language-en-us') - fwrite($lang_file, "\n"); - $target_lang = $lang_code[1]; - if (strlen($target_lang) == 2) { - if (array_key_exists($target_lang, $this->legacy_map)) { - $target_lang = $this->legacy_map[$target_lang]; - } - } - $spacer = ""; - if (strlen($target_lang) == 11) - $spacer = " "; - $language_name = $this->escape_str(array_shift($text[$lang_label])); - if (empty($language_name)) - $language_name = $this->escape_str($target_lang); - fwrite($lang_file, "\$text['language-$target_lang'$spacer]['en-us'] = \"$language_name\";\n"); - } - else { - - //put a line break in between the last tag if it has changed - if ($last_lang_label != $lang_label) - fwrite($lang_file, "\n"); - foreach ($this->languages as $lang_code) { - $value = ""; - $append = ""; - $spacer = ""; - $target_lang = $lang_code; - if (strlen($lang_code) == 2) { - if (array_key_exists($lang_code, $this->legacy_map)) { - $target_lang = $this->legacy_map[$lang_code]; - } - } - if (strlen($target_lang) == 2) - $spacer = " "; - if (array_key_exists($lang_code, $text[$lang_label])) - $value = $text[$lang_label][$lang_code]; - if (empty($value) and array_key_exists($target_lang, $this->legacy_map)) { - $value = $text[$lang_label][$this->legacy_map[$target_lang]]; - } - $base_code = substr($target_lang, 0, 2); - if (!empty($value) - and array_key_exists($base_code, $this->legacy_map ) - and $this->legacy_map[$base_code] != $target_lang - and $value == $text[$lang_label][$this->legacy_map[$base_code]] - ) { - $append = " //copied from ".$this->legacy_map[$base_code]; - } - if (empty($value)) { - foreach($this->languages as $lang_code) { - if (substr($lang_code, 0, 2) == $base_code and !empty($text[$lang_label][$lang_code])) { - $value = $text[$lang_label][$lang_code]; - $append = " //copied from $lang_code"; - continue; - } - } - } - if(empty($append) && array_key_exists($comment[$lang_label], $lang_code)) { - $append = " //$comment[$lang_label][$lang_code]"; - } - fwrite($lang_file, "\$text['$lang_label']['$target_lang'$spacer] = \"".$this->escape_str($value)."\";$append\n"); - } - } - $last_lang_label = $lang_label; - } - - //close the language file - fwrite($lang_file, "\n?>\n"); - fclose($lang_file); - } - - /** - * Detect all languages from the application and session language settings. - * - * @param bool $no_sort Flag to prevent sorting of detected languages. - * - * @return void - */ - public function detect_all_languages($no_sort = false) { - - //clear $text ready for the import - $text = array(); - $languages = array(); - - //retrieve all the languages - $files = glob($_SERVER["PROJECT_ROOT"] . "/*/*/app_languages.php"); - foreach($files as $file) { - include $file; - } - include $_SERVER["PROJECT_ROOT"] . "/resources/app_languages.php"; - - //check every tag - foreach($text as $lang_codes) { - foreach($lang_codes as $language_code => $value) { - if (strlen($language_code) == 2) { - if (array_key_exists($language_code, $this->legacy_map)) { - $language_code = $this->legacy_map[$language_code]; - } - } - $languages[$language_code] = 1; - } - } - - //set $this->languages up according to what we found - unset($languages['en-us']); - $languages = array_keys($languages); + unset($text['language-name']['en-us']); + if (is_array($text['language-name'])) { + $languages = array_keys($text['language-name']); asort($languages); array_unshift($languages, 'en-us'); + } //support legacy variable + if (is_array($languages)) { $_SESSION['app']['languages'] = $languages; $this->languages = $languages; - - //rewrite resources/app_languges - $this->organize_language('resources', $no_sort); - } - - /** - * Get totals of language usage across all languages and applications. - * - * This method retrieves the total number of translations, menu items, - * and application descriptions for each language, as well as a total count - * for each category. - * - * @return array A nested array containing the total counts for languages, menu items, and app descriptions - */ - public function language_totals() { - - //setup variables - $language_totals = array(); - $language_totals['languages']['total'] = 0; - $language_totals['menu_items']['total'] = 0; - $language_totals['app_descriptions']['total'] = 0; - foreach ($this->languages as $language_code) { - $language_totals[$language_code] = 0; - } - - //retrieve all the languages - $text = array(); - $files = glob($_SERVER["PROJECT_ROOT"] . "/*/*/app_languages.php"); - foreach($files as $file) { - include $file; - } - include $_SERVER["PROJECT_ROOT"] . "/resources/app_languages.php"; - - //check every tag - foreach($text as $label_name => $values) { - $language_totals['languages']['total']++; - foreach ($this->languages as $language_code) { - if (!empty($values[$language_code])) - $language_totals['languages'][$language_code]++; - } - } - unset($text); - - //retrieve all the menus - $x = 0; - $files = glob($_SERVER["PROJECT_ROOT"] . "/*/*"); - foreach($files as $file) { - if (file_exists($file . "/app_menu.php")) - include $file . "/app_menu.php"; - if (file_exists($file . "/app_config.php")) - include $file . "/app_config.php"; - $x++; - } - - //check every tag - foreach ($apps as $app) { - $language_totals['app_descriptions']['total']++; - foreach($app['menu'] as $menu_item) { - $language_totals['menu_items']['total']++; - foreach ($this->languages as $language_code) { - if (!empty($menu_item['title'][$language_code])) - $language_totals['menu_items'][$language_code]++; - } - } - foreach ($this->languages as $language_code) { - if (!empty($app['description'][$language_code])) { - $language_totals['app_descriptions'][$language_code]++; - } - } - } - - return $language_totals; - } - - private function escape_str($string = '') { - //perform initial escape - $string = addslashes(stripslashes($string)); - //swap \' as we don't need to escape those - return preg_replace("/\\\'/", "'", $string); - //escape " as we write our strings double quoted - return preg_replace("/\"/", '\"', $string); + } } /** * The clear_cache method is called automatically for any class that implements the clear_cache interface. - * The function declared here ensures that all clear_cache methods have the same number of parameters being passed, which in this case, is no parameters. + * The function declared here ensures that all clear_cache methods have the same number of parameters being passed, + * which in this case, are no parameters. */ public static function clear_cache() { //check for apcu extension and if is enabled @@ -485,4 +147,356 @@ class text implements clear_cache { } } } + + /** + * Get a specific language from the language file + * + * @param string|null $language_code examples: en-us, es-cl, fr-fr, pt-pt + * @param string|null $app_path examples: app/exec or core/domains + * @param bool $exclude_global Exclude the global languages file + * + * @return array A flattened array containing the desired language + */ + public function get(?string $language_code = null, ?string $app_path = null, bool $exclude_global = false) { + + //define the text array + $text = []; + + //check the session language + if ($language_code == null) { + $language_code = $this->settings->get('domain', 'language', 'en-us'); + } + + //check the language code + if (strlen($language_code) == 2 && array_key_exists($language_code, $this->legacy_map)) { + $language_code = $this->legacy_map[$language_code]; + } + + //get the app_languages.php + if ($app_path != null) { + $lang_path = $_SERVER["PROJECT_ROOT"] . "/" . $app_path; + } else { + $lang_path = getcwd(); + } + + //check the class cache + $cache_key = "text_{$language_code}_{$lang_path}"; + if ($this->apcu_enabled && apcu_exists($cache_key)) { + return apcu_fetch($cache_key); + + } + if (isset(self::$tanslated[$cache_key])) { + return self::$tanslated[$cache_key]; + } + + //get the global app_languages.php + if (!$exclude_global && file_exists($_SERVER["PROJECT_ROOT"] . "/resources/app_languages.php")) { + require $_SERVER["PROJECT_ROOT"] . "/resources/app_languages.php"; + } + + if (file_exists($lang_path . "/app_languages.php") && ($lang_path != 'resources' or $exclude_global)) { + include "{$lang_path}/app_languages.php"; + } + //else { + // throw new Exception("could not find app_languages for '$app_path'"); + //} + + //reduce to specific language + if ($language_code != 'all' && is_array($text)) { + foreach ($text as $key => $value) { + if (isset($value[$language_code]) && !empty($value[$language_code])) { + //use the selected language + $text[$key] = $value[$language_code]; + } elseif (isset($value['en-us'])) { + //fallback to en-us + $text[$key] = $value['en-us']; + } else { + $text[$key] = ''; + } + } + } + + //cache the reduced language using the file as a key + if ($this->apcu_enabled) { + apcu_store($cache_key, $text); + } else { + self::$tanslated[$cache_key] = $text; + } + + //return the array of translations + return $text; + } + + /** + * Detect all languages from the application and session language settings. + * + * @param bool $no_sort Flag to prevent sorting of detected languages. + * + * @return void + */ + public function detect_all_languages($no_sort = false) { + + //clear $text ready for the import + $text = []; + $languages = []; + + //retrieve all the languages + $files = glob($_SERVER["PROJECT_ROOT"] . "/*/*/app_languages.php"); + foreach ($files as $file) { + include $file; + } + include $_SERVER["PROJECT_ROOT"] . "/resources/app_languages.php"; + + //check every tag + foreach ($text as $lang_codes) { + foreach ($lang_codes as $language_code => $value) { + if (strlen($language_code) == 2) { + if (array_key_exists($language_code, $this->legacy_map)) { + $language_code = $this->legacy_map[$language_code]; + } + } + $languages[$language_code] = 1; + } + } + + //set $this->languages up according to what we found + unset($languages['en-us']); + $languages = array_keys($languages); + asort($languages); + array_unshift($languages, 'en-us'); + + //support legacy variable + $_SESSION['app']['languages'] = $languages; + $this->languages = $languages; + + //rewrite resources/app_languges + $this->organize_language('resources', $no_sort); + } + + /** + * Organize a specific language from the language file. + * + * @param string $app_path Path to the application where the language file is located. + * @param bool $no_sort Flag to determine if the text should be sorted or not. + * + * @return void + */ + public function organize_language($app_path = null, $no_sort = false) { + + //clear $text ready for the import + $text = []; + + //get the app_languages.php + if ($app_path == null) { + throw new Exception("\$app_path must be specified"); + } + $lang_path = $_SERVER["PROJECT_ROOT"] . "/$app_path/app_languages.php"; + if (!file_exists($lang_path)) { + throw new Exception("could not find app_languages for '$app_path'"); + } + require $lang_path; + + if (!is_array($text)) { + throw new Exception("failed to import text data from '$app_path'"); + } + + //collect existing comments + $comment = []; + $file_handle = fopen($lang_path, "r"); + while (!feof($file_handle)) { + if (preg_match('/\$text\[[\'"](.+)[\'"]\]\[[\'"](.+)[\'"]]\s+=\s+[\'"].*[\'"];\s+\/\/(.+)/', fgets($file_handle), $matches)) { + $comment[$matches[0]][$matches[1]] = $matches[2]; + } + } + fclose($file_handle); + + //open the language file for writing + $lang_file = fopen($lang_path, 'w'); + date_default_timezone_set('UTC'); + fwrite($lang_file, "languages as $language) { + $temp_B["language-$language"] = $text["language-$language"]; + unset($text["language-$language"]); + } + $temp_C["language-en-us"] = $temp_B["language-en-us"]; + unset($temp_B["language-en-us"]); + ksort($temp_B); + $temp_B = array_merge($temp_C, $temp_B); + ksort($text); + $text = array_merge($temp_A, $temp_B, $text); + unset($temp_A, $temp_B, $temp_C); + } else { + ksort($text); + } + } else { + if ($app_path == 'resources') { + foreach ($this->languages as $language) { + $label = array_shift($text["language-$language"]); + if (empty($label)) + $label = $language; + $text["language-$language"]['en-us'] = $label; + } + } + } + $last_lang_label = ""; + foreach ($text as $lang_label => $lang_codes) { + + //behave differently if we are one of the special language-* tags + if (preg_match('/\Alanguage-(\w{2}|\w{2}-\w{2})\z/', $lang_label, $lang_code)) { + if ($lang_label == 'language-en-us') + fwrite($lang_file, "\n"); + $target_lang = $lang_code[1]; + if (strlen($target_lang) == 2) { + if (array_key_exists($target_lang, $this->legacy_map)) { + $target_lang = $this->legacy_map[$target_lang]; + } + } + $spacer = ""; + if (strlen($target_lang) == 11) + $spacer = " "; + $language_name = $this->escape_str(array_shift($text[$lang_label])); + if (empty($language_name)) + $language_name = $this->escape_str($target_lang); + fwrite($lang_file, "\$text['language-$target_lang'$spacer]['en-us'] = \"$language_name\";\n"); + } else { + + //put a line break in between the last tag if it has changed + if ($last_lang_label != $lang_label) + fwrite($lang_file, "\n"); + foreach ($this->languages as $lang_code) { + $value = ""; + $append = ""; + $spacer = ""; + $target_lang = $lang_code; + if (strlen($lang_code) == 2) { + if (array_key_exists($lang_code, $this->legacy_map)) { + $target_lang = $this->legacy_map[$lang_code]; + } + } + if (strlen($target_lang) == 2) + $spacer = " "; + if (array_key_exists($lang_code, $text[$lang_label])) + $value = $text[$lang_label][$lang_code]; + if (empty($value) and array_key_exists($target_lang, $this->legacy_map)) { + $value = $text[$lang_label][$this->legacy_map[$target_lang]]; + } + $base_code = substr($target_lang, 0, 2); + if (!empty($value) + and array_key_exists($base_code, $this->legacy_map) + and $this->legacy_map[$base_code] != $target_lang + and $value == $text[$lang_label][$this->legacy_map[$base_code]] + ) { + $append = " //copied from " . $this->legacy_map[$base_code]; + } + if (empty($value)) { + foreach ($this->languages as $lang_code) { + if (substr($lang_code, 0, 2) == $base_code and !empty($text[$lang_label][$lang_code])) { + $value = $text[$lang_label][$lang_code]; + $append = " //copied from $lang_code"; + continue; + } + } + } + if (empty($append) && array_key_exists($comment[$lang_label], $lang_code)) { + $append = " //$comment[$lang_label][$lang_code]"; + } + fwrite($lang_file, "\$text['$lang_label']['$target_lang'$spacer] = \"" . $this->escape_str($value) . "\";$append\n"); + } + } + $last_lang_label = $lang_label; + } + + //close the language file + fwrite($lang_file, "\n?>\n"); + fclose($lang_file); + } + + /** + * Escapes special characters in a string for use in a SQL query. + * + * @param string $string The input string to be escaped. Defaults to an empty string if not provided. + * + * @return string The escaped string. + */ + private function escape_str($string = '') { + //perform initial escape + $string = addslashes(stripslashes($string)); + //swap \' as we don't need to escape those + return preg_replace("/\\\'/", "'", $string); + //escape " as we write our strings double quoted + return preg_replace("/\"/", '\"', $string); + } + + /** + * Get totals of language usage across all languages and applications. + * + * This method retrieves the total number of translations, menu items, + * and application descriptions for each language, as well as a total count + * for each category. + * + * @return array A nested array containing the total counts for languages, menu items, and app descriptions + */ + public function language_totals() { + + //setup variables + $language_totals = []; + $language_totals['languages']['total'] = 0; + $language_totals['menu_items']['total'] = 0; + $language_totals['app_descriptions']['total'] = 0; + foreach ($this->languages as $language_code) { + $language_totals[$language_code] = 0; + } + + //retrieve all the languages + $text = []; + $files = glob($_SERVER["PROJECT_ROOT"] . "/*/*/app_languages.php"); + foreach ($files as $file) { + include $file; + } + include $_SERVER["PROJECT_ROOT"] . "/resources/app_languages.php"; + + //check every tag + foreach ($text as $label_name => $values) { + $language_totals['languages']['total']++; + foreach ($this->languages as $language_code) { + if (!empty($values[$language_code])) + $language_totals['languages'][$language_code]++; + } + } + unset($text); + + //retrieve all the menus + $x = 0; + $files = glob($_SERVER["PROJECT_ROOT"] . "/*/*"); + foreach ($files as $file) { + if (file_exists($file . "/app_menu.php")) + include $file . "/app_menu.php"; + if (file_exists($file . "/app_config.php")) + include $file . "/app_config.php"; + $x++; + } + + //check every tag + foreach ($apps as $app) { + $language_totals['app_descriptions']['total']++; + foreach ($app['menu'] as $menu_item) { + $language_totals['menu_items']['total']++; + foreach ($this->languages as $language_code) { + if (!empty($menu_item['title'][$language_code])) + $language_totals['menu_items'][$language_code]++; + } + } + foreach ($this->languages as $language_code) { + if (!empty($app['description'][$language_code])) { + $language_totals['app_descriptions'][$language_code]++; + } + } + } + + return $language_totals; + } } diff --git a/resources/classes/token.php b/resources/classes/token.php index a0b05160f6..87d6b4e009 100644 --- a/resources/classes/token.php +++ b/resources/classes/token.php @@ -32,78 +32,42 @@ class token { /** - * Called when the object is created - */ + * Called when the object is created + */ //public $code; /** - * Class constructor - */ + * Class constructor + */ public function __construct() { } /** * Create the token + * * @var string $key */ public function create($key) { //clear previously validated tokens - $this->clear_validated(); + $this->clear_validated(); //allow only specific characters - $key = preg_replace('[^a-zA-Z0-9\-_@.\/]', '', $key); + $key = preg_replace('[^a-zA-Z0-9\-_@.\/]', '', $key); //create a token for the key submitted - $token = [ - 'name'=>hash_hmac('sha256', $key, bin2hex(random_bytes(32))), - 'hash'=>hash_hmac('sha256', $key, bin2hex(random_bytes(32))), - 'validated'=>false - ]; + $token = [ + 'name' => hash_hmac('sha256', $key, bin2hex(random_bytes(32))), + 'hash' => hash_hmac('sha256', $key, bin2hex(random_bytes(32))), + 'validated' => false, + ]; //save in the token session array - $_SESSION['tokens'][$key][] = $token; + $_SESSION['tokens'][$key][] = $token; //send the hash - return $token; - - } - - /** - * validate the token - * @var string $key - * @var string $value - */ - public function validate($key, $value = '') { - - //allow only specific characters - $key = preg_replace('[^a-zA-Z0-9]', '', $key); - - //get the token name - if (!empty($_SESSION['tokens']) && is_array($_SESSION['tokens'][$key]) && @sizeof($_SESSION['tokens'][$key]) != 0) { - foreach ($_SESSION['tokens'][$key] as $t => $token) { - $token_name = $token['name']; - if (isset($_REQUEST[$token_name])) { - $value = $_REQUEST[$token_name]; - break; - } - } - } - - //limit the value to specific characters - $value = preg_replace('[^a-zA-Z0-9]', '', $value); - - //compare the hashed tokens - if (!empty($_SESSION['tokens']) && is_array($_SESSION['tokens'][$key]) && @sizeof($_SESSION['tokens'][$key]) != 0) { - foreach ($_SESSION['tokens'][$key] as $t => $token) { - if (hash_equals($token['hash'], $value)) { - $_SESSION['tokens'][$key][$t]['validated'] = true; - return true; - } - } - } - return false; + return $token; } @@ -124,6 +88,44 @@ class token { } } + /** + * validate the token + * + * @var string $key + * @var string $value + */ + public function validate($key, $value = '') { + + //allow only specific characters + $key = preg_replace('[^a-zA-Z0-9]', '', $key); + + //get the token name + if (!empty($_SESSION['tokens']) && is_array($_SESSION['tokens'][$key]) && @sizeof($_SESSION['tokens'][$key]) != 0) { + foreach ($_SESSION['tokens'][$key] as $t => $token) { + $token_name = $token['name']; + if (isset($_REQUEST[$token_name])) { + $value = $_REQUEST[$token_name]; + break; + } + } + } + + //limit the value to specific characters + $value = preg_replace('[^a-zA-Z0-9]', '', $value); + + //compare the hashed tokens + if (!empty($_SESSION['tokens']) && is_array($_SESSION['tokens'][$key]) && @sizeof($_SESSION['tokens'][$key]) != 0) { + foreach ($_SESSION['tokens'][$key] as $t => $token) { + if (hash_equals($token['hash'], $value)) { + $_SESSION['tokens'][$key][$t]['validated'] = true; + return true; + } + } + } + return false; + + } + } /* @@ -147,5 +149,3 @@ echo " \ No newline at end of file diff --git a/resources/classes/tones.php b/resources/classes/tones.php index 26c1ebea80..c53949159a 100644 --- a/resources/classes/tones.php +++ b/resources/classes/tones.php @@ -25,54 +25,60 @@ Matthew Vale */ - class tones { +class tones { - /** - * declare private variables - */ - private $music_list; - private $recordings_list; - private $default_tone_label; - private $database; + /** + * declare private variables + */ + private $music_list; + private $recordings_list; + private $default_tone_label; + private $database; - /** - * called when the object is created - */ - public function __construct(array $setting_array = []) { - //add multi-lingual support - $language = new text; - $text = $language->get(); + /** + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $setting_array An optional array of settings to override default values. Defaults to []. + */ + public function __construct(array $setting_array = []) { + //add multi-lingual support + $language = new text; + $text = $language->get(); - //connect to the database - $this->database = $setting_array['database'] ?? database::new(); - } - - /** - * tones_list function - * - * @return array - */ - public function tones_list() { - //get the tones - $sql = "select * from v_vars "; - $sql .= "where var_category = 'Tones' "; - $sql .= "order by var_name asc "; - $tones = $this->database->select($sql, null, 'all'); - if (!empty($tones)) { - foreach ($tones as $tone) { - $tone = $tone['var_name']; - if (isset($text['label-'.$tone])) { - $label = $text['label-'.$tone]; - } - else { - $label = $tone; - } - $tone_list[$tone] = $label; - } - } - unset($sql, $tones, $tone); - - //return the tones - return $tone_list ?? []; - } + //connect to the database + $this->database = $setting_array['database'] ?? database::new(); } + + /** + * Retrieves a list of tone names with their corresponding labels. + * + * This method fetches tone data from the database and formats it for display. + * + * @return array An array of tone names as keys and their labels as values. If no tones are found, an empty array + * is returned. + */ + public function tones_list() { + //get the tones + $sql = "select * from v_vars "; + $sql .= "where var_category = 'Tones' "; + $sql .= "order by var_name asc "; + $tones = $this->database->select($sql, null, 'all'); + if (!empty($tones)) { + foreach ($tones as $tone) { + $tone = $tone['var_name']; + if (isset($text['label-' . $tone])) { + $label = $text['label-' . $tone]; + } else { + $label = $tone; + } + $tone_list[$tone] = $label; + } + } + unset($sql, $tones, $tone); + + //return the tones + return $tone_list ?? []; + } +} diff --git a/resources/classes/user.php b/resources/classes/user.php index f731ba0290..3dad6a5194 100644 --- a/resources/classes/user.php +++ b/resources/classes/user.php @@ -3,18 +3,26 @@ /* * user class - used to store user groups, permissions, and other values */ + class user { + public $domain_uuid; + public $domain_name; + public $username; + public $user_email; + public $contact_uuid; private $database; - public $domain_uuid; - public $domain_name; private $user_uuid; private $permissions; private $groups; - public $username; - public $user_email; - public $contact_uuid; + /** + * Constructor for the class. + * + * This method initializes the object with setting_array and session data. + * + * @param array $setting_array An optional array of settings to override default values. Defaults to []. + */ public function __construct(database $database, $domain_uuid, $user_uuid) { //set the database variable @@ -30,7 +38,7 @@ class user { $this->user_uuid = $user_uuid; } - //set the user groups, permission and details + //set the user groups, permission, and details if (isset($domain_uuid) && is_uuid($domain_uuid) && isset($user_uuid) && is_uuid($user_uuid)) { $this->set_groups(); $this->set_permissions(); @@ -41,6 +49,16 @@ class user { /* * set_details method sets the user assigned details */ + /** + * Sets the user details based on the domain UUID and user UUID. + * + * This method queries the database to retrieve the user's details, + * including their domain name, username, email address, and contact UUID. + * + * @access public + * + * @return bool True if the query is successful, false otherwise. + */ public function set_details() { $sql = "select d.domain_name, u.username, u.user_email, u.contact_uuid "; $sql .= "from v_users as u, v_domains as d "; @@ -62,6 +80,11 @@ class user { /* * get_user_uuid method gets the user_uuid */ + /** + * Retrieves the user's UUID. + * + * @return string The user's unique identifier in UUID format. + */ public function get_user_uuid() { return $this->user_uuid; } @@ -69,31 +92,55 @@ class user { /* * set_permissions method sets the user assigned permissions */ - public function set_permissions() { - $this->permissions = new permissions($this->database, $this->domain_uuid, $this->user_uuid); - } - /* - * get_permissions method gets the user assigned permissions - */ + /** + * Retrieves the permissions associated with this entity. + * + * @return array An array of permission objects or identifiers. + * @access public + */ public function get_permissions() { return $this->permissions->get_permissions(); } + /* + * get_permissions method gets the user assigned permissions + */ + + /** + * Sets the user's permissions. + * + * @access public + * @return void + */ + public function set_permissions() { + $this->permissions = new permissions($this->database, $this->domain_uuid, $this->user_uuid); + } + /* * set_groups method sets the user assigned groups */ - public function set_groups() { - $this->groups = new groups($this->database, $this->domain_uuid, $this->user_uuid); + + /** + * Retrieves the user's groups. + * + * @return array An array of group objects that the user belongs to. + */ + public function get_groups() { + return $this->groups->get_groups(); } /* * get_groups method gets the user assigned groups */ - public function get_groups() { - return $this->groups->get_groups(); + + /** + * Sets the user's group assignments. + * + * @return void + */ + public function set_groups() { + $this->groups = new groups($this->database, $this->domain_uuid, $this->user_uuid); } } - -?> diff --git a/resources/classes/vcard.php b/resources/classes/vcard.php index d8a54ab30f..34e887367f 100644 --- a/resources/classes/vcard.php +++ b/resources/classes/vcard.php @@ -1,10 +1,17 @@ log = "New vcard() called
    "; - $this->data = array( - "display_name"=>null - ,"first_name"=>null - ,"last_name"=>null - ,"additional_name"=>null - ,"name_prefix"=>null - ,"name_suffix"=>null - ,"nickname"=>null - ,"title"=>null - ,"role"=>null - ,"department"=>null - ,"company"=>null - ,"work_po_box"=>null - ,"work_extended_address"=>null - ,"work_address"=>null - ,"work_city"=>null - ,"work_state"=>null - ,"work_postal_code"=>null - ,"work_country"=>null - ,"home_po_box"=>null - ,"home_extended_address"=>null - ,"home_address"=>null - ,"home_city"=>null - ,"home_state"=>null - ,"home_postal_code"=>null - ,"home_country"=>null - ,"voice_tel"=>null - ,"work_tel"=>null - ,"home_tel"=>null - ,"cell_tel"=>null - ,"fax_tel"=>null - ,"pager_tel"=>null - ,"email1"=>null - ,"email2"=>null - ,"url"=>null - ,"photo"=>null - ,"birthday"=>null - ,"timezone"=>null - ,"sort_string"=>null - ,"note"=>null - ); + $this->log = "New vcard() called
    "; + $this->data = [ + "display_name" => null + , "first_name" => null + , "last_name" => null + , "additional_name" => null + , "name_prefix" => null + , "name_suffix" => null + , "nickname" => null + , "title" => null + , "role" => null + , "department" => null + , "company" => null + , "work_po_box" => null + , "work_extended_address" => null + , "work_address" => null + , "work_city" => null + , "work_state" => null + , "work_postal_code" => null + , "work_country" => null + , "home_po_box" => null + , "home_extended_address" => null + , "home_address" => null + , "home_city" => null + , "home_state" => null + , "home_postal_code" => null + , "home_country" => null + , "voice_tel" => null + , "work_tel" => null + , "home_tel" => null + , "cell_tel" => null + , "fax_tel" => null + , "pager_tel" => null + , "email1" => null + , "email2" => null + , "url" => null + , "photo" => null + , "birthday" => null + , "timezone" => null + , "sort_string" => null + , "note" => null, + ]; return true; } @@ -66,109 +76,169 @@ class vcard { build() method checks all the values, builds appropriate defaults for missing values, generates the vcard data string. */ + + /** + * Downloads the vCard data as a file. + * + * @access public + * @return bool True on successful download, false otherwise. + */ + function download() { + $this->log .= "vcard download() called
    "; + if (!$this->card) { + $this->build(); + } + if (!$this->filename) { + $this->filename = trim($this->data['display_name']); + } + $this->filename = str_replace(" ", "_", $this->filename); + header("Content-type: text/directory"); + header("Content-Disposition: attachment; filename=" . $this->filename . ".vcf"); + header("Pragma: public"); + echo $this->card; + return true; + } + + /* + download() method streams the vcard to the browser client. + */ + + /** + * Builds the vCard. + * + * @access public + * @return string The built vCard as a string. + */ function build() { $this->log .= "vcard build() called
    "; /* For many of the values, if they are not passed in, we set defaults or build them based on other values. */ - if (!$this->class) { $this->class = "PUBLIC"; } - if (!$this->data['display_name']) { - $this->data['display_name'] = trim($this->data['first_name']." ".$this->data['last_name']); + if (!$this->class) { + $this->class = "PUBLIC"; + } + if (!$this->data['display_name']) { + $this->data['display_name'] = trim($this->data['first_name'] . " " . $this->data['last_name']); + } + if (!$this->data['sort_string']) { + $this->data['sort_string'] = $this->data['last_name']; + } + if (!$this->data['sort_string']) { + $this->data['sort_string'] = $this->data['company']; + } + if (!$this->data['timezone']) { + $this->data['timezone'] = date("O"); + } + if (!$this->revision_date) { + $this->revision_date = date('Y-m-d H:i:s'); } - if (!$this->data['sort_string']) { $this->data['sort_string'] = $this->data['last_name']; } - if (!$this->data['sort_string']) { $this->data['sort_string'] = $this->data['company']; } - if (!$this->data['timezone']) { $this->data['timezone'] = date("O"); } - if (!$this->revision_date) { $this->revision_date = date('Y-m-d H:i:s'); } $this->card = "BEGIN:VCARD\r\n"; $this->card .= "VERSION:3.0\r\n"; //$this->card .= "CLASS:".$this->class."\r\n"; //$this->card .= "PRODID:-//class_vcard from TroyWolf.com//NONSGML Version 1//EN\r\n"; // $this->card .= "REV:".$this->revision_date."\r\n"; - $this->card .= "FN:".$this->data['display_name']."\r\n"; + $this->card .= "FN:" . $this->data['display_name'] . "\r\n"; $this->card .= "N:"; - $this->card .= $this->data['last_name'].";"; + $this->card .= $this->data['last_name'] . ";"; $this->card .= $this->data['first_name']; if (!empty($this->data['additional_name'])) { - $this->card .= ";".$this->data['additional_name']; + $this->card .= ";" . $this->data['additional_name']; } if (!empty($this->data['name_prefix'])) { - $this->card .= ";".$this->data['name_prefix']; + $this->card .= ";" . $this->data['name_prefix']; } if (!empty($this->data['name_suffix'])) { - $this->card .= ";".$this->data['name_suffix']; + $this->card .= ";" . $this->data['name_suffix']; } $this->card .= "\r\n"; - if ($this->data['nickname']) { $this->card .= "NICKNAME:".$this->data['contact_nickname']."\r\n"; } - if ($this->data['title']) { $this->card .= "TITLE:".$this->data['title']."\r\n"; } - if ($this->data['company']) { $this->card .= "ORG:".$this->data['company']; } - if ($this->data['department']) { $this->card .= ";".$this->data['department']; } + if ($this->data['nickname']) { + $this->card .= "NICKNAME:" . $this->data['contact_nickname'] . "\r\n"; + } + if ($this->data['title']) { + $this->card .= "TITLE:" . $this->data['title'] . "\r\n"; + } + if ($this->data['company']) { + $this->card .= "ORG:" . $this->data['company']; + } + if ($this->data['department']) { + $this->card .= ";" . $this->data['department']; + } $this->card .= "\r\n"; - $vcard_address_type_values = array('work','home','dom','intl','postal','parcel','pref'); + $vcard_address_type_values = ['work', 'home', 'dom', 'intl', 'postal', 'parcel', 'pref']; foreach ($vcard_address_type_values as $vcard_address_type_value) { - if (!empty($this->data[$vcard_address_type_value.'_po_box']) - || !empty($this->data[$vcard_address_type_value.'_extended_address']) - || !empty($this->data[$vcard_address_type_value.'_address']) - || !empty($this->data[$vcard_address_type_value.'_city']) - || !empty($this->data[$vcard_address_type_value.'_state']) - || !empty($this->data[$vcard_address_type_value.'_postal_code']) - || !empty($this->data[$vcard_address_type_value.'_country'])) { - $this->card .= "ADR;TYPE=".$vcard_address_type_value.":"; - if (!empty($this->data[$vcard_address_type_value.'_po_box'])) { - $this->card .= $this->data[$vcard_address_type_value.'_po_box'].";"; + if (!empty($this->data[$vcard_address_type_value . '_po_box']) + || !empty($this->data[$vcard_address_type_value . '_extended_address']) + || !empty($this->data[$vcard_address_type_value . '_address']) + || !empty($this->data[$vcard_address_type_value . '_city']) + || !empty($this->data[$vcard_address_type_value . '_state']) + || !empty($this->data[$vcard_address_type_value . '_postal_code']) + || !empty($this->data[$vcard_address_type_value . '_country'])) { + $this->card .= "ADR;TYPE=" . $vcard_address_type_value . ":"; + if (!empty($this->data[$vcard_address_type_value . '_po_box'])) { + $this->card .= $this->data[$vcard_address_type_value . '_po_box'] . ";"; } - if (!empty($this->data[$vcard_address_type_value.'_extended_address'])) { - $this->card .= $this->data[$vcard_address_type_value.'_extended_address'].";"; + if (!empty($this->data[$vcard_address_type_value . '_extended_address'])) { + $this->card .= $this->data[$vcard_address_type_value . '_extended_address'] . ";"; } - if (!empty($this->data[$vcard_address_type_value.'_address'])) { - $this->card .= $this->data[$vcard_address_type_value.'_address'].";"; + if (!empty($this->data[$vcard_address_type_value . '_address'])) { + $this->card .= $this->data[$vcard_address_type_value . '_address'] . ";"; } - if (!empty($this->data[$vcard_address_type_value.'_city'])) { - $this->card .= $this->data[$vcard_address_type_value.'_city'].";"; + if (!empty($this->data[$vcard_address_type_value . '_city'])) { + $this->card .= $this->data[$vcard_address_type_value . '_city'] . ";"; } - if (!empty($this->data[$vcard_address_type_value.'_state'])) { - $this->card .= $this->data[$vcard_address_type_value.'_state'].";"; + if (!empty($this->data[$vcard_address_type_value . '_state'])) { + $this->card .= $this->data[$vcard_address_type_value . '_state'] . ";"; } - if (!empty($this->data[$vcard_address_type_value.'_postal_code'])) { - $this->card .= $this->data[$vcard_address_type_value.'_postal_code'].";"; + if (!empty($this->data[$vcard_address_type_value . '_postal_code'])) { + $this->card .= $this->data[$vcard_address_type_value . '_postal_code'] . ";"; } - if (!empty($this->data[$vcard_address_type_value.'_country'])) { - $this->card .= $this->data[$vcard_address_type_value.'_country'].""; + if (!empty($this->data[$vcard_address_type_value . '_country'])) { + $this->card .= $this->data[$vcard_address_type_value . '_country'] . ""; } $this->card .= "\r\n"; } } - if ($this->data['email1']) { $this->card .= "EMAIL;PREF=1:".$this->data['email1']."\r\n"; } - if ($this->data['email2']) { $this->card .= "EMAIL;PREF=2:".$this->data['email2']."\r\n"; } - if ($this->data['voice_tel']) { $this->card .= "TEL;TYPE=voice:".$this->data['voice_tel']."\r\n"; } - if ($this->data['work_tel']) { $this->card .= "TEL;TYPE=work:".$this->data['work_tel']."\r\n"; } - if ($this->data['home_tel']) { $this->card .= "TEL;TYPE=home:".$this->data['home_tel']."\r\n"; } - if ($this->data['cell_tel']) { $this->card .= "TEL;TYPE=cell:".$this->data['cell_tel']."\r\n"; } - if ($this->data['fax_tel']) { $this->card .= "TEL;TYPE=fax:".$this->data['fax_tel']."\r\n"; } - if ($this->data['pager_tel']) { $this->card .= "TEL;TYPE=pager:".$this->data['pager_tel']."\r\n"; } - if ($this->data['url']) { $this->card .= "URL:".$this->data['url']."\r\n"; } - if ($this->data['birthday']) { $this->card .= "BDAY:".$this->data['birthday']."\r\n"; } - if ($this->data['role']) { $this->card .= "ROLE:".$this->data['role']."\r\n"; } - if ($this->data['note']) { $this->card .= "NOTE:".$this->data['note']."\r\n"; } - $this->card .= "TZ:".$this->data['timezone']."\r\n"; + if ($this->data['email1']) { + $this->card .= "EMAIL;PREF=1:" . $this->data['email1'] . "\r\n"; + } + if ($this->data['email2']) { + $this->card .= "EMAIL;PREF=2:" . $this->data['email2'] . "\r\n"; + } + if ($this->data['voice_tel']) { + $this->card .= "TEL;TYPE=voice:" . $this->data['voice_tel'] . "\r\n"; + } + if ($this->data['work_tel']) { + $this->card .= "TEL;TYPE=work:" . $this->data['work_tel'] . "\r\n"; + } + if ($this->data['home_tel']) { + $this->card .= "TEL;TYPE=home:" . $this->data['home_tel'] . "\r\n"; + } + if ($this->data['cell_tel']) { + $this->card .= "TEL;TYPE=cell:" . $this->data['cell_tel'] . "\r\n"; + } + if ($this->data['fax_tel']) { + $this->card .= "TEL;TYPE=fax:" . $this->data['fax_tel'] . "\r\n"; + } + if ($this->data['pager_tel']) { + $this->card .= "TEL;TYPE=pager:" . $this->data['pager_tel'] . "\r\n"; + } + if ($this->data['url']) { + $this->card .= "URL:" . $this->data['url'] . "\r\n"; + } + if ($this->data['birthday']) { + $this->card .= "BDAY:" . $this->data['birthday'] . "\r\n"; + } + if ($this->data['role']) { + $this->card .= "ROLE:" . $this->data['role'] . "\r\n"; + } + if ($this->data['note']) { + $this->card .= "NOTE:" . $this->data['note'] . "\r\n"; + } + $this->card .= "TZ:" . $this->data['timezone'] . "\r\n"; $this->card .= "END:VCARD"; } - - /* - download() method streams the vcard to the browser client. - */ - function download() { - $this->log .= "vcard download() called
    "; - if (!$this->card) { $this->build(); } - if (!$this->filename) { $this->filename = trim($this->data['display_name']); } - $this->filename = str_replace(" ", "_", $this->filename); - header("Content-type: text/directory"); - header("Content-Disposition: attachment; filename=".$this->filename.".vcf"); - header("Pragma: public"); - echo $this->card; - return true; - } } diff --git a/resources/classes/waveform.php b/resources/classes/waveform.php index 2e7cbba081..bc1afcd7dc 100644 --- a/resources/classes/waveform.php +++ b/resources/classes/waveform.php @@ -2,120 +2,80 @@ /** * * - * @author MaximAL - * @since 2019-02-13 Added `$onePhase` parameters to get only positive waveform data and image - * @since 2018-10-22 Added `getWaveformData()` method and `$soxCommand` configuration - * @since 2016-11-21 - * @date 2016-11-21 - * @time 19:08 - * @link http://maximals.ru - * @link http://sijeko.ru - * @link https://github.com/maximal/audio-waveform-php + * @author MaximAL + * @since 2019-02-13 Added `$onePhase` parameters to get only positive waveform data and image + * @since 2018-10-22 Added `getWaveformData()` method and `$soxCommand` configuration + * @since 2016-11-21 + * @date 2016-11-21 + * @time 19:08 + * @link http://maximals.ru + * @link http://sijeko.ru + * @link https://github.com/maximal/audio-waveform-php * @copyright © MaximAL, Sijeko 2016-2019 * - * @modified fusionate - * @since 2024-02-07 Added option to return image in base64 format by setting $filename to 'base64' - * @since 2024-02-08 Added `$singleAxis` parameter to combine channels (if stereo) into single axis - * @since 2024-02-08 Added `$colorA` and `$colorB` parameters to allow different colors for each channel - * @since 2024-02-08 Rename `$onePhase` parameter to `$singlePhase` and change to public static variable for class - * @since 2024-02-08 Modified singleAxis so channel 2 would display as negative waveform data when singlePhase enabled + * @modified fusionate + * @since 2024-02-07 Added option to return image in base64 format by setting $filename to 'base64' + * @since 2024-02-08 Added `$singleAxis` parameter to combine channels (if stereo) into single axis + * @since 2024-02-08 Added `$colorA` and `$colorB` parameters to allow different colors for each channel + * @since 2024-02-08 Rename `$onePhase` parameter to `$singlePhase` and change to public static variable for class + * @since 2024-02-08 Modified singleAxis so channel 2 would display as negative waveform data when singlePhase + * enabled * * */ namespace maximal\audio; +use Exception; + /** * Waveform class allows you to get waveform data and images from audio files + * * @package maximal\audio */ -class Waveform -{ - protected $filename; - protected $info; - protected $channels; - protected $samples; - protected $sampleRate; - protected $duration; - +class Waveform { public static $linesPerPixel = 8; public static $samplesPerLine = 512; - public static $singlePhase; // set `true` to get positive waveform phase only, `false` to get both positive and negative waveform phases - public static $singleAxis; // combine double or single phases to use same axis +public static $singlePhase; +public static $singleAxis; + public static $color = [95, 95, 95, 0.5]; +public static $colorA; +public static $colorB; + public static $backgroundColor = [245, 245, 245, 1]; + public static $axisColor = [0, 0, 0, 0.1]; // set `true` to get positive waveform phase only, `false` to get both positive and negative waveform phases + public static $soxCommand = 'sox'; // combine double or single phases to use same axis // Colors in CSS `rgba(red, green, blue, opacity)` format - public static $color = [95, 95, 95, 0.5]; - public static $colorA; // color of left channel (1) - public static $colorB; // color of right channel (2) - public static $backgroundColor = [245, 245, 245, 1]; - public static $axisColor = [0, 0, 0, 0.1]; + protected $filename; + protected $info; // color of left channel (1) + protected $channels; // color of right channel (2) + protected $samples; + protected $sampleRate; // SoX command: 'sox', '/usr/local/bin/sox' etc - public static $soxCommand = 'sox'; + protected $duration; - - public function __construct($filename) - { + /** + * Initializes a new instance of this class with the specified filename. + * + * @param string $filename The name of the file associated with this instance. + * + * @access public + */ + public function __construct($filename) { $this->filename = $filename; } - public function getInfo() - { - $out = null; - $ret = null; - exec(self::$soxCommand . ' --i ' . escapeshellarg($this->filename) . ' 2>&1', $out, $ret); - $str = implode('|', $out); - - $match = null; - if (preg_match('/Channels?\s*\:\s*(\d+)/ui', $str, $match)) { - $this->channels = intval($match[1]); - } - - $match = null; - if (preg_match('/Sample\s*Rate\s*\:\s*(\d+)/ui', $str, $match)) { - $this->sampleRate = intval($match[1]); - } - - $match = null; - if (preg_match('/Duration.*[^\d](\d+)\s*samples?/ui', $str, $match)) { - $this->samples = intval($match[1]); - } - - if ($this->samples && $this->sampleRate) { - $this->duration = 1.0 * $this->samples / $this->sampleRate; - } - - if ($ret !== 0) { - throw new \Exception('Failed to get audio info.' . PHP_EOL . 'Error: ' . implode(PHP_EOL, $out) . PHP_EOL); - } - } - - public function getSampleRate() - { - if (!$this->sampleRate) { - $this->getInfo(); - } - return $this->sampleRate; - } - - public function getChannels() - { - if (!$this->channels) { - $this->getInfo(); - } - return $this->channels; - } - - public function getSamples() - { - if (!$this->samples) { - $this->getInfo(); - } - return $this->samples; - } - - public function getDuration() - { + /** + * Retrieves the duration of the current media file. + * + * If the duration has not been retrieved yet, it will be fetched from the media server. + * + * @return float The duration of the media file in seconds. + * + * @access public + */ + public function getDuration() { if (!$this->duration) { $this->getInfo(); } @@ -124,14 +84,15 @@ class Waveform /** * Get waveform from the audio file. + * * @param string $filename Image file name - * @param int $width Width of the image file in pixels - * @param int $height Height of the image file in pixels + * @param int $width Width of the image file in pixels + * @param int $height Height of the image file in pixels + * * @return bool Returns `true` on success or `false` on failure, when generating an image file, or a base64 string. * @throws \Exception */ - public function getWaveform($filename, $width, $height) - { + public function getWaveform($filename, $width, $height) { // Calculating parameters $needChannels = $this->getChannels() > 1 ? 2 : 1; $data = $this->getWaveformData($width, self::$singlePhase ?? false); @@ -159,7 +120,7 @@ class Waveform $center1 = $center2 = $height / 2; } else { if (self::$singlePhase ?? false) { - $center1 = $needChannels === 2 ? $height / 2 - 1: $height - 1; + $center1 = $needChannels === 2 ? $height / 2 - 1 : $height - 1; $center2 = $needChannels === 2 ? $height - 1 : null; } else { $center1 = $needChannels === 2 ? ($height / 2 - 1) / 2 : $height / 2; @@ -218,14 +179,31 @@ class Waveform } } + /** + * Retrieves a collection of channels. + * + * If no channels have been loaded yet, the {@link getInfo()} method is called to load them first. + * + * @return array A collection of channel objects. + * + * @access public + */ + public function getChannels() { + if (!$this->channels) { + $this->getInfo(); + } + return $this->channels; + } + /** * Get waveform data from the audio file. + * * @param int $width Desired width of the image file in pixels + * * @return array * @throws \Exception */ - public function getWaveformData($width) - { + public function getWaveformData($width) { // Calculating parameters $needChannels = $this->getChannels() > 1 ? 2 : 1; $samplesPerPixel = self::$samplesPerLine * self::$linesPerPixel; @@ -249,7 +227,7 @@ class Waveform $pipes = null; $proc = proc_open($command, $outputs, $pipes); if (!$proc) { - throw new \Exception('Failed to run `sox` command'); + throw new Exception('Failed to run `sox` command'); } $lines1 = []; @@ -260,26 +238,26 @@ class Waveform $channel2 = []; foreach ($data as $index => $sample) { if ($needChannels === 2 && $index % 2 === 0) { - $channel2 []= $sample; + $channel2 [] = $sample; } else { - $channel1 []= $sample; + $channel1 [] = $sample; } } if (self::$singlePhase ?? false) { // Rectifying to get positive values only - $lines1 []= abs(min($channel1)); - $lines1 []= abs(max($channel1)); + $lines1 [] = abs(min($channel1)); + $lines1 [] = abs(max($channel1)); if ($needChannels === 2) { - $lines2 []= abs(min($channel2)); - $lines2 []= abs(max($channel2)); + $lines2 [] = abs(min($channel2)); + $lines2 [] = abs(max($channel2)); } } else { // Two phases - $lines1 []= min($channel1); - $lines1 []= max($channel1); + $lines1 [] = min($channel1); + $lines1 [] = max($channel1); if ($needChannels === 2) { - $lines2 []= min($channel2); - $lines2 []= max($channel2); + $lines2 [] = min($channel2); + $lines2 [] = max($channel2); } } } @@ -288,14 +266,92 @@ class Waveform $ret = proc_close($proc); if ($ret !== 0) { - throw new \Exception('Failed to run `sox` command. Error:' . PHP_EOL . $err); + throw new Exception('Failed to run `sox` command. Error:' . PHP_EOL . $err); } return ['lines1' => $lines1, 'lines2' => $lines2]; } - public static function rgbaToColor($img, $rgba) - { + /** + * Retrieves the sample rate associated with this instance. + * + * If the sample rate has not been previously retrieved, it will be obtained + * by calling getInfo(). The sample rate is then cached for future retrieval. + * + * @return int|null The sample rate in Hz, or null if unable to retrieve the information. + * + * @access public + */ + public function getSampleRate() { + if (!$this->sampleRate) { + $this->getInfo(); + } + return $this->sampleRate; + } + + /** + * Retrieves information about the audio file associated with this instance. + * + * @access public + */ + public function getInfo() { + $out = null; + $ret = null; + exec(self::$soxCommand . ' --i ' . escapeshellarg($this->filename) . ' 2>&1', $out, $ret); + $str = implode('|', $out); + + $match = null; + if (preg_match('/Channels?\s*\:\s*(\d+)/ui', $str, $match)) { + $this->channels = intval($match[1]); + } + + $match = null; + if (preg_match('/Sample\s*Rate\s*\:\s*(\d+)/ui', $str, $match)) { + $this->sampleRate = intval($match[1]); + } + + $match = null; + if (preg_match('/Duration.*[^\d](\d+)\s*samples?/ui', $str, $match)) { + $this->samples = intval($match[1]); + } + + if ($this->samples && $this->sampleRate) { + $this->duration = 1.0 * $this->samples / $this->sampleRate; + } + + if ($ret !== 0) { + throw new Exception('Failed to get audio info.' . PHP_EOL . 'Error: ' . implode(PHP_EOL, $out) . PHP_EOL); + } + } + + /** + * Retrieves a collection of sample data. + * + * If no samples have been retrieved yet, this method will call getInfo() to populate the internal samples list. + * + * @return int The collection of sample data. + * + * @access public + */ + public function getSamples() { + if (!$this->samples) { + $this->getInfo(); + } + return $this->samples; + } + + /** + * Converts an RGBA color to a PHP image color with alpha channel. + * + * @param resource $img The PHP image resource to convert the color for. + * @param array $rgba An array containing the red, green, blue and alpha values of the color. + * + * @return int The allocated color index. + * + * @access public + * @static + */ + public static function rgbaToColor($img, $rgba) { return imagecolorallocatealpha($img, $rgba[0], $rgba[1], $rgba[2], round((1 - $rgba[3]) * 127)); } } diff --git a/resources/classes/xml.php b/resources/classes/xml.php index 7a1942c023..7ae16e6969 100644 --- a/resources/classes/xml.php +++ b/resources/classes/xml.php @@ -1,18 +1,20 @@