value pairs that can be used by any Resource * or Element. */ public $placeholders= []; /** * @var modResource An instance of the current modResource controlling the * request. */ public $resource= null; /** * @var string The preferred Culture key for the current request. */ public $cultureKey= ''; /** * @var modLexicon Represents a localized dictionary of common words and phrases. */ public $lexicon= null; /** * @var modUser The current user object, if one is authenticated for the * current request and context. */ public $user= null; /** * @var array Represents the modContentType instances that can be delivered * by this modX deployment. */ public $contentTypes= null; /** * @var mixed The resource id or alias being requested. */ public $resourceIdentifier= null; /** * @var string The method to use to locate the Resource, 'id' or 'alias'. */ public $resourceMethod= null; /** * @var boolean Indicates if the resource was generated during this request. */ public $resourceGenerated= false; /** * @var array Version information for this MODX deployment. */ public $version= null; /** * @var string Unique site id for each MODX installation. */ public $site_id; /** * @var string Unique uuid for each MODX installation. */ public $uuid; /** * @var boolean Indicates if modX has been successfully initialized for a * modContext. */ protected $_initialized= false; /** * @var array An array of javascript content to be inserted into the HEAD * of an HTML resource. */ public $sjscripts= []; /** * @var array An array of javascript content to be inserted into the BODY * of an HTML resource. */ public $jscripts= []; /** * @var array An array of already loaded javascript/css code */ public $loadedjscripts= []; /** * @var string Stores the virtual path for a request to MODX if the * friendly_alias_paths option is enabled. */ public $virtualDir; /** * @var modErrorHandler|object An error_handler for the modX instance. * @deprecated Use the service/DI container to access the `error_handler` service. */ public $errorHandler= null; /** * @var modError An error response class for the request */ public $error = null; /** * @var modManagerController A controller object that represents a page in the manager */ public $controller = null; /** * @var modRegistry $registry */ public $registry; /** * @var modMail $mail */ public $mail; /** * @var modSmarty $smarty */ public $smarty; /** * @var array $processors An array of loaded processors and their class name */ public $processors = []; /** * @var array An array of regex patterns regulary cleansed from content. */ public $sanitizePatterns = [ 'scripts' => '@]*?>.*?@si', 'entities' => '@&#(\d+);@', 'tags1' => '@\[\[(?:(?!(\[\[|\]\])).)*\]\]@si', 'tags2' => '@(\[\[|\]\])@si', ]; /** * @var integer An integer representing the session state of modX. */ protected $_sessionState= modX::SESSION_STATE_UNINITIALIZED; /** * @var array A config array that stores the bootstrap settings. */ protected $_config= null; /** * @var array A config array that stores the system-wide settings. */ public $_systemConfig= []; /** * @var array A config array that stores the user settings. */ public $_userConfig= []; /** * @var int The current log sequence */ protected $_logSequence= 0; /** * @var array An array of plugins that have been cached for execution */ public $pluginCache= []; /** * @var array The element source cache used for caching and preparing Element data */ public $sourceCache= [ modChunk::class => [] ,modSnippet::class => [] ,modTemplateVar::class => [], ]; /** @var modCacheManager $cacheManager */ public $cacheManager; /** * @deprecated * @var modSystemEvent $Event */ public $Event= null; /** * @deprecated * @var string $documentOutput */ public $documentOutput= null; /** * Keeps an in-memory representation of what deprecated functions have been logged * for this request, to avoid spamming the log too often. See the `deprecated` method. * * @var array */ private $_deprecations = []; /** * Harden the environment against common security flaws. * * @static */ public static function protect() { $targets= ['PHP_SELF', 'HTTP_USER_AGENT', 'HTTP_REFERER', 'QUERY_STRING']; foreach ($targets as $target) { $_SERVER[$target] = isset ($_SERVER[$target]) ? htmlspecialchars($_SERVER[$target], ENT_QUOTES) : null; } } /** * Sanitize values of an array using regular expression patterns. * * @static * @param array $target The target array to sanitize. * @param array|string $patterns A regular expression pattern, or array of * regular expression patterns to apply to all values of the target. * @param integer $depth The maximum recursive depth to sanitize if the * target contains values that are arrays. * @param integer $nesting The maximum nesting level in which to dive * @return array The sanitized array. */ public static function sanitize(array &$target, array $patterns= [], $depth= 99, $nesting= 10) { foreach ($target as $key => &$value) { if (is_array($value) && $depth > 0) { modX :: sanitize($value, $patterns, $depth-1); } elseif (is_string($value)) { if (!empty($patterns)) { $iteration = 1; $nesting = ((int) $nesting ? (int) $nesting : 10); while ($iteration <= $nesting) { $matched = false; foreach ($patterns as $pattern) { $patternIterator = 1; $patternMatches = preg_match($pattern, $value); if ($patternMatches > 0) { $matched = true; while ($patternMatches > 0 && $patternIterator <= $nesting) { $value= preg_replace($pattern, '', $value); $patternMatches = preg_match($pattern, $value); } } } if (!$matched) { break; } $iteration++; } } $target[$key]= $value; } } return $target; } /** * Validate a cultureKey, returning a safe default if it is not a * well-formed language or locale identifier. * * A cultureKey may only contain the characters valid in a locale identifier * (e.g. en, de-DE, pt_BR). This guards values that are consumed before * request sanitization and/or used to build lexicon paths, so a request * value cannot inject MODX tags or path-traversal sequences. * * @static * @param mixed $cultureKey The culture key to validate. * @param string $default The value to return when $cultureKey is invalid. * @return string The validated culture key, or $default. */ public static function sanitizeCultureKey($cultureKey, string $default = 'en'): string { if (!is_string($cultureKey) || $cultureKey === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $cultureKey)) { return $default; } return $cultureKey; } /** * @param array|string $data The target data to sanitize. * @param array $replaceable * @return array|string The sanitized data */ public static function replaceReserved($data, array $replaceable = ['[' => '[', ']' => ']', '`' => '`']) { if (\is_array($data)) { $result = []; foreach ($data as $key => &$value) { $key = self::replaceReserved($key, $replaceable); $result[$key] = self::replaceReserved($value, $replaceable); } } elseif (\is_scalar($data)) { $result = \str_replace(\array_keys($replaceable), \array_values($replaceable), $data); } else { $result = ''; } return $result; } /** * Sanitizes a string * * @param string $str The string to sanitize * @param array $chars An array of chars to remove * @param string $allowedTags A list of tags to allow. * @return string The sanitized string. */ public function sanitizeString($str,$chars = ['/',"'",'"','(',')',';','>','<'],$allowedTags = '') { $str = str_replace($chars,'',strip_tags($str,$allowedTags)); return preg_replace("/[^A-Za-z0-9_\-\.\/\\p{L}[\p{L} _.-]/u",'',$str); } /** * Turn an associative or numeric array into a valid query string. * * @static * @param array $parameters An associative or numeric-indexed array of parameters. * @param string $numPrefix A string prefix added to the numeric-indexed array keys. * Ignored if associative array is used. * @param string $argSeparator The string used to separate arguments in the resulting query string. * @return string A valid query string representing the parameters. */ public static function toQueryString(array $parameters = [], $numPrefix = '', $argSeparator = '&') { return http_build_query($parameters, $numPrefix, $argSeparator); } /** * Evaluate boolean-like parameter value and return it as an actual boolean or as a string; a string may be * needed/preferred when building code blocks (e.g., javascript) via php strings (nowdoc/heredoc ones in particular). * * @param array $params An associative or numeric-indexed array of parameters * @param string|int $key The array key to evaluate * @param bool $returnBoolAsString Whether to return the string representation ('true' or 'false') of the boolean value * @return string|bool */ public function paramValueIsTrue(array $params, $key, bool $returnBoolAsString = false) { if (isset($params[$key]) && in_array($params[$key], ['true', true, '1', 1], true)) { return $returnBoolAsString ? 'true' : true ; } return $returnBoolAsString ? 'false' : false ; } /** * Create, retrieve, or update specific modX instances. * * @static * @param string|int|null $id An optional identifier for the instance. If not set * a uniqid will be generated and used as the key for the instance. * @param array|null $config An optional array of config data for the instance. * @param bool $forceNew If true a new instance will be created even if an instance * with the provided $id already exists in modX::$instances. * @return static An instance of modX. * @throws xPDOException */ public static function getInstance($id = null, $config = null, $forceNew = false) { $class = static::class; if (is_null($id)) { if (!is_null($config) || $forceNew || empty(self::$instances)) { $id = uniqid($class); } else { $instances =& self::$instances; $id = key($instances); } } if ($forceNew || !array_key_exists($id, self::$instances) || !(self::$instances[$id] instanceof $class)) { self::$instances[$id] = new $class('', $config); } elseif (self::$instances[$id] instanceof $class && is_array($config)) { self::$instances[$id]->config = array_merge(self::$instances[$id]->config, $config); } if (!(self::$instances[$id] instanceof $class)) { throw new xPDOException("Error getting {$class} instance, id = {$id}"); } return self::$instances[$id]; } /** * Construct a new modX instance. * * @param string $configPath An absolute filesystem path to look for the config file. * @param array $options xPDO options that can be passed to the instance. * @param array $driverOptions PDO driver options that can be passed to the instance. */ public function __construct($configPath= '', $options = null, $driverOptions = null) { try { $options = $this->loadConfig($configPath, $options, $driverOptions); parent :: __construct( null, null, null, $options, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT) ); $this->setLogLevel($this->getOption('log_level', null, xPDO::LOG_LEVEL_ERROR)); $this->setLogTarget($this->getOption('log_target', null, 'FILE')); $debug = $this->getOption('debug'); if (!is_null($debug) && $debug !== '') { $this->setDebug($debug); } $this->setPackage('MODX\Revolution', MODX_CORE_PATH . 'src/', null, 'MODX\\'); $this->addPackage('MODX\Revolution\Registry\Db', MODX_CORE_PATH . 'src/', null, 'MODX\\'); $this->addPackage('MODX\Revolution\Sources', MODX_CORE_PATH . 'src/', null, 'MODX\\'); $this->addPackage('MODX\Revolution\Transport', MODX_CORE_PATH . 'src/', null, 'MODX\\'); } catch (xPDOException $xe) { $this->sendError('unavailable', ['error_message' => $xe->getMessage()]); } catch (Exception $e) { $this->sendError('unavailable', ['error_message' => $e->getMessage()]); } } /** * Load the modX configuration when creating an instance of modX. * * @param string $configPath An absolute path location to search for the modX config file. * @param array $data Data provided to initialize the instance with, overriding config file entries. * @param array $driverOptions Driver options for the primary connection. * * @return Container A DI container containing the config data ready for use by the modX::__construct() method. * @throws xPDOException */ protected function loadConfig($configPath = '', $data = [], $driverOptions = array()) { if (!is_array($data)) $data = []; modX :: protect(); if (!defined('MODX_CONFIG_KEY')) { define('MODX_CONFIG_KEY', 'config'); } if (empty ($configPath)) { $configPath= MODX_CORE_PATH . 'config/'; } global $database_dsn, $database_user, $database_password, $config_options, $driver_options, $table_prefix, $site_id, $uuid; if (file_exists($configPath . MODX_CONFIG_KEY . '.inc.php') && include ($configPath . MODX_CONFIG_KEY . '.inc.php')) { $cachePath= MODX_CORE_PATH . 'cache/'; if (MODX_CONFIG_KEY !== 'config') $cachePath .= MODX_CONFIG_KEY . '/'; if (!is_array($config_options)) $config_options = []; if (!is_array($driver_options)) $driver_options = []; if (!is_array($driverOptions)) $driverOptions = []; $driver_options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT) + $driverOptions + $driver_options; $data = array_merge( [ xPDO::OPT_CACHE_KEY => 'default', xPDO::OPT_CACHE_HANDLER => xPDOFileCache::class, xPDO::OPT_CACHE_PATH => $cachePath, xPDO::OPT_TABLE_PREFIX => $table_prefix, xPDO::OPT_HYDRATE_FIELDS => true, xPDO::OPT_HYDRATE_RELATED_OBJECTS => true, xPDO::OPT_HYDRATE_ADHOC_FIELDS => true, xPDO::OPT_VALIDATOR_CLASS => modValidator::class, xPDO::OPT_VALIDATE_ON_SAVE => true, 'cache_system_settings' => true, 'cache_system_settings_key' => 'system_settings', 'load_deprecated_global_class_aliases' => true, ], $config_options, $data ); $primaryConnection = [ 'dsn' => $database_dsn, 'username' => $database_user, 'password' => $database_password, 'options' => [ xPDO::OPT_CONN_MUTABLE => isset($data[xPDO::OPT_CONN_MUTABLE]) ? (bool) $data[xPDO::OPT_CONN_MUTABLE] : true, ], 'driverOptions' => $driver_options, ]; if (!array_key_exists(xPDO::OPT_CONNECTIONS, $data) || !is_array($data[xPDO::OPT_CONNECTIONS])) { $data[xPDO::OPT_CONNECTIONS] = []; } array_unshift($data[xPDO::OPT_CONNECTIONS], $primaryConnection); if (!empty($site_id)) $this->site_id = $site_id; if (!empty($uuid)) $this->uuid = $uuid; if ((bool)$data['load_deprecated_global_class_aliases']) { include_once MODX_CORE_PATH . 'include/deprecated.php'; } $container = new Container(); $container['config'] = $data; return $container; } else { throw new xPDOException("Could not load MODX config file."); } } /** * Initializes the modX engine. * * This includes preparing the session, pre-loading some common * classes and objects, the current site and context settings, extension * packages used to override session handling, error handling, or other * initialization classes * * @param string $contextKey Indicates the context to initialize. * @param array|null $options An array of options for the initialization. * @return bool True if initialized successfully, or already initialized. */ public function initialize($contextKey= 'web', $options = null) { if (!$this->_initialized) { if (!$this->startTime) { $this->startTime= microtime(true); } $this->getCacheManager(); $this->getConfig(); if (!$this->getOption(xPDO::OPT_SETUP)) { $this->_initNamespaces(); $this->_initContext($contextKey, false, $options); $this->_loadExtensionPackages($options); } $this->_initSession($options); $this->_initErrorHandler($options); $this->_initHttpClient(); $this->_initCulture($options); $this->services->add('registry', new modRegistry($this)); $this->registry = $this->services->get('registry'); $this->services->add(modManagerDateFormatter::class, fn() => new modManagerDateFormatter($this)); if (!$this->getOption(xPDO::OPT_SETUP)) { $this->invokeEvent( 'OnMODXInit', [ 'contextKey' => $contextKey, 'options' => $options, ] ); } if (is_array ($this->config)) { $c = $this->config; if ((bool)$this->getOption('filter_config_placeholders', null, true)) { unset($c['password'], $c['username'], $c['mail_smtp_pass'], $c['mail_smtp_user'], $c['proxy_password'], $c['proxy_username'], $c['connections'], $c['connection_init'], $c['connection_mutable'], $c['dbname'], $c['database'], $c['table_prefix'], $c['driverOptions'], $c['dsn'], $c['session_name'], $c['cache_path'], $c['connectors_path'], $c['friendly_alias_translit_class_path'], $c['manager_path'], $c['processors_path']); } $this->setPlaceholders($c, '+'); } $this->_initialized= true; } return $this->_initialized; } /** * Initialize namespaces. */ protected function _initNamespaces() { $namespaces = $this->call(modNamespace::class, 'loadCache', [&$this]); $modx =& $this; foreach ($namespaces as $namespace) { if (is_readable($namespace['path'] . 'bootstrap.php')) { require $namespace['path'] . 'bootstrap.php'; } } } /** * Loads any extension packages. * * @param array|null An optional array of options that can contain additional * extension packages which will be merged with those specified via config. */ protected function _loadExtensionPackages($options = null) { $cache = $this->call(modExtensionPackage::class, 'loadCache', [&$this]); if (!empty($cache)) { foreach ($cache as $package) { $package['table_prefix'] = isset($package['table_prefix']) ? $package['table_prefix'] : null; $this->addPackage($package['namespace'],$package['path'],$package['table_prefix']); if (!empty($package['service_name']) && !empty($package['service_class'])) { $this->getService($package['service_name'],$package['service_class'],$package['path']); } } } $this->_loadExtensionPackagesDeprecated($options); } /** * Load system-setting based extension packages. This is not recommended; use modExtensionPackage from 2.3 onward. * The System Setting will be automatically removed in 2.4/3.0 and no longer functional. * * @deprecated To be removed in 2.4/3.0 * @param null $options */ protected function _loadExtensionPackagesDeprecated($options = null) { $extPackages = $this->getOption('extension_packages'); $extPackages = $this->fromJSON($extPackages); if (!is_array($extPackages)) $extPackages = []; if (is_array($options) && array_key_exists('extension_packages', $options)) { $optPackages = $this->fromJSON($options['extension_packages']); if (is_array($optPackages)) { $extPackages = array_merge($extPackages, $optPackages); } } if (!empty($extPackages)) { foreach ($extPackages as $extPackage) { if (!is_array($extPackage)) continue; foreach ($extPackage as $packageName => $package) { if (!empty($package) && !empty($package['path'])) { $package['tablePrefix'] = isset($package['tablePrefix']) ? $package['tablePrefix'] : null; $package['path'] = str_replace( [ '[[++core_path]]', '[[++base_path]]', '[[++assets_path]]', '[[++manager_path]]', ], [ $this->config['core_path'], $this->config['base_path'], $this->config['assets_path'], $this->config['manager_path'], ], $package['path']); $this->addPackage($packageName,$package['path'],$package['tablePrefix']); if (!empty($package['serviceName']) && !empty($package['serviceClass'])) { $packagePath = str_replace('//','/',$package['path'].$packageName.'/'); $this->getService($package['serviceName'],$package['serviceClass'],$packagePath); } } } } } } /** * Sets the debugging features of the modX instance. * * @param boolean|int $debug Boolean or bitwise integer describing the * debug state and/or PHP error reporting level. * * @return boolean|int The previous value. * * @info PHP errors are handle by modErrorHandler with at most LOG_LEVEL_INFO * When called by modX $debug is a string (ie $this->getOption('debug')) * * (bool)true , (string)true , (string)-1 -> LOG_LEVEL_DEBUG (MODX), E_ALL | E_STRICT (PHP) * (bool)false, (string)false, (string) 0 -> LOG_LEVEL_ERROR (MODX), 0 (PHP) * (int)nnn -> LOG_LEVEL_INFO (MODX), nnn (PHP) * (string)E_XXX -> LOG_LEVEL_INFO (MODX), E_XXX (PHP) */ public function setDebug($debug= true) { $oldValue= $this->getDebug(); if (($debug === true) || ('true' === $debug) || ('-1' === $debug)) { error_reporting(-1); parent :: setDebug(true); } else { if (($debug === false) || ('false' === $debug) || ('0' === $debug)) { error_reporting(0); parent :: setDebug(false); } else { $debug = (is_int($debug) ? $debug : (defined($debug) ? intval(constant($debug)) : 0)); if ($debug) { error_reporting($debug); parent :: setLogLevel(xPDO::LOG_LEVEL_INFO); } } } return $oldValue; } /** * Set a PSR-3 logger for this MODX instance. * * When a PSR-3 logger is set, it replaces legacy FILE/ECHO/HTML output for * all log messages that pass the configured logLevel threshold. modRegister * logging is preserved alongside the PSR-3 logger. * * @param LoggerInterface $logger A PSR-3 compatible logger instance. */ public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } /** * Get an extended xPDOCacheManager instance responsible for MODX caching. * * @param string $class The class name of the cache manager to load * @param array $options An array of options to send to the cache manager instance * @return modCacheManager A modCacheManager instance registered for this modX instance. */ public function getCacheManager($class= 'xPDO\\Cache\\xPDOCacheManager', $options = ['path' => XPDO_CORE_PATH, 'ignorePkg' => true] ) { if ($this->cacheManager === null) { $className = $this->getOption('modCacheManager.class', null, modCacheManager::class); if ($this->cacheManager = new $className($this)) { $this->_cacheEnabled = true; } } return $this->cacheManager; } /** * Gets the MODX parser. * * Returns an instance of modParser responsible for parsing tags in element * content, performing actions, returning content and/or sending other responses * in the process. * * @return modParser The modParser for this modX instance. */ public function getParser() { if (!$this->services->has('parser')) { $parserClass = $this->getOption('modParser.class', null, modParser::class); $this->services->add('parser', new $parserClass($this)); } if (!$this->parser instanceof modParser) { $this->parser = $this->services->get('parser'); } return $this->parser; } /** * Gets all of the parent resource ids for a given resource. * * @param integer $id The resource id for the starting node. * @param integer $height How many levels max to search for parents (default 10). * @param array $options An array of filtering options, such as 'context' to specify the context to grab from * @return array An array of all the parent resource ids for the specified resource. */ public function getParentIds($id= null, $height= 10,array $options = []) { $parentId= 0; $parents= []; if ($id && $height > 0) { $context = ''; if (!empty($options['context'])) { $this->getContext($options['context']); $context = $options['context']; } $resourceMap = !empty($context) && !empty($this->contexts[$context]->resourceMap) ? $this->contexts[$context]->resourceMap : $this->resourceMap; foreach ($resourceMap as $parentId => $mapNode) { if (array_search($id, $mapNode) !== false) { $parents[]= $parentId; break; } } if ($parentId && !empty($parents)) { $height--; $parents= array_merge($parents, $this->getParentIds($parentId,$height,$options)); } } return $parents; } /** * Gets all of the child resource ids for a given resource. * * @see getTree for hierarchical node results * @param integer $id The resource id for the starting node. * @param integer $depth How many levels max to search for children (default 10). * @param array $options An array of filtering options, such as 'context' to specify the context to grab from * @return array An array of all the child resource ids for the specified resource. */ public function getChildIds($id= null, $depth= 10,array $options = []) { $children= []; if ($id !== null && intval($depth) >= 1) { $id= is_int($id) ? $id : intval($id); $context = ''; if (!empty($options['context'])) { $this->getContext($options['context']); $context = $options['context']; } $resourceMap = !empty($context) && !empty($this->contexts[$context]->resourceMap) ? $this->contexts[$context]->resourceMap : $this->resourceMap; if (isset ($resourceMap["{$id}"])) { if ($children= $resourceMap["{$id}"]) { foreach ($children as $child) { if ($c = $this->getChildIds($child, $depth - 1, $options)) { $children = array_merge($children, $c); } } } } } return $children; } /** * Get a site tree from a single or multiple modResource instances. * * @see getChildIds for linear results * @param int|array $id A single or multiple modResource ids to build the * tree from. * @param int $depth The maximum depth to build the tree (default 10). * @param array $options An array of filtering options, such as 'context' to specify the context to grab from * @return array An array containing the tree structure. */ public function getTree($id= null, $depth= 10, array $options = []) { $tree= []; if (!empty($options['context'])) { $this->getContext($options['context']); } if ($id !== null) { if (is_array ($id)) { foreach ($id as $k => $v) { $tree[$v] = $this->getTree($v, $depth - 1, $options); } } elseif ($branch= $this->getChildIds($id, 1, $options)) { foreach ($branch as $key => $child) { if ($depth > 0 && $leaf = $this->getTree($child, $depth - 1, $options)) { $tree[$child]= $leaf; } else { $tree[$child]= $child; } } } } return $tree; } /** * Sets a placeholder value. * * @param string $key The unique string key which identifies the * placeholder. * @param mixed $value The value to set the placeholder to. */ public function setPlaceholder($key, $value) { if (is_string($key)) { $this->placeholders["{$key}"]= $value; } } /** * Sets a collection of placeholders stored in an array or as object vars. * * An optional namespace parameter can be prepended to each placeholder key in the collection, * to isolate the collection of placeholders. * * Note that unlike toPlaceholders(), this function does not add separators between the * namespace and the placeholder key. Use toPlaceholders() when working with multi-dimensional * arrays or objects with variables other than scalars so each level gets delimited by a * separator. * * @param array|object $placeholders An array of values or object to set as placeholders. * @param string $namespace A namespace prefix to prepend to each placeholder key. */ public function setPlaceholders($placeholders, $namespace= '') { $this->toPlaceholders($placeholders, $namespace, ''); } /** * Sets placeholders from values stored in arrays and objects. * * Each recursive level adds to the prefix, building an access path using an optional separator. * * @param array|object $subject An array or object to process. * @param string $prefix An optional prefix to be prepended to the placeholder keys. Recursive * calls prepend the parent keys. * @param string $separator A separator to place in between the prefixes and keys. Default is a * dot or period: '.'. * @param boolean $restore Set to true if you want overwritten placeholder values returned. * @return array A multi-dimensional array containing up to two elements: 'keys' which always * contains an array of placeholder keys that were set, and optionally, if the restore parameter * is true, 'restore' containing an array of placeholder values that were overwritten by the method. */ public function toPlaceholders($subject, $prefix= '', $separator= '.', $restore= false) { $keys = []; $restored = []; if (is_object($subject)) { if ($subject instanceof xPDOObject) { $subject= $subject->toArray(); } else { $subject= get_object_vars($subject); } } if (is_array($subject)) { foreach ($subject as $key => $value) { $rv = $this->toPlaceholder($key, $value, $prefix, $separator, $restore); if (isset($rv['keys'])) { foreach ($rv['keys'] as $rvKey) $keys[] = $rvKey; } if ($restore === true && isset($rv['restore'])) { $restored = array_merge($restored, $rv['restore']); } } } $return = ['keys' => $keys]; if ($restore === true) $return['restore'] = $restored; return $return; } /** * Recursively validates and sets placeholders appropriate to the value type passed. * * @param string $key The key identifying the value. * @param mixed $value The value to set. * @param string $prefix A string prefix to prepend to the key. Recursive calls prepend the * parent keys as well. * @param string $separator A separator placed in between the prefix and the key. Default is a * dot or period: '.'. * @param boolean $restore Set to true if you want overwritten placeholder values returned. * @return array A multi-dimensional array containing up to two elements: 'keys' which always * contains an array of placeholder keys that were set, and optionally, if the restore parameter * is true, 'restore' containing an array of placeholder values that were overwritten by the method. */ public function toPlaceholder($key, $value, $prefix= '', $separator= '.', $restore= false) { $return = ['keys' => []]; if ($restore === true) $return['restore'] = []; if (!empty($prefix) && !empty($separator)) { $prefix .= $separator; } if (is_array($value) || is_object($value)) { $return = $this->toPlaceholders($value, "{$prefix}{$key}", $separator, $restore); } elseif (is_scalar($value)) { $return['keys'][] = "{$prefix}{$key}"; if ($restore === true && array_key_exists("{$prefix}{$key}", $this->placeholders)) { $return['restore']["{$prefix}{$key}"] = $this->getPlaceholder("{$prefix}{$key}"); } $this->setPlaceholder("{$prefix}{$key}", $value); } return $return; } /** * Get a placeholder value by key. * * @param string $key The key of the placeholder to a return a value from. * @return mixed The value of the requested placeholder, or an empty string if not located. */ public function getPlaceholder($key) { $placeholder= null; if (is_string($key) && array_key_exists($key, $this->placeholders)) { $placeholder= & $this->placeholders["{$key}"]; } return $placeholder; } /** * Unset a placeholder value by key. * * @param string $key The key of the placeholder to unset. */ public function unsetPlaceholder($key) { if (is_string($key) && array_key_exists($key, $this->placeholders)) { unset($this->placeholders[$key]); } } /** * Unset multiple placeholders, either by prefix or an array of keys. * * @param string|array $keys A string prefix or an array of keys indicating * the placeholders to unset. */ public function unsetPlaceholders($keys) { if (is_array($keys)) { foreach ($keys as $key) { if (is_string($key)) $this->unsetPlaceholder($key); if (is_array($key)) $this->unsetPlaceholders($key); } } elseif (is_string($keys)) { $placeholderKeys = array_keys($this->placeholders); foreach ($placeholderKeys as $key) { if (strpos($key, $keys) === 0) $this->unsetPlaceholder($key); } } } /** * Returns the full table name (with dynamic prefix) based on database settings. * Legacy - Useful when dealing with migrations or prefixed database tables without an xPDO model (which xPDO.getTableName requires.) * * @param string $table Name of MODX table, less table prefix. * @return string Full table name containing database and table prefix. */ public function getFullTableName( $table = '' ) { return $this->getOption('dbname') .".". $this->getOption( xPDO::OPT_TABLE_PREFIX ) . $table; } /** * Generates a URL representing a specified resource. * * @param integer $id The id of a resource. * @param string $context Specifies a context to limit URL generation to. * @param string $args A query string to append to the generated URL. * @param mixed $scheme The scheme indicates in what format the URL is generated.
*
     *      -1 : (default value) URL is relative to site_url
     *       0 : see http
     *       1 : see https
     *    full : URL is absolute, prepended with site_url from config
     *     abs : URL is absolute, prepended with base_url from config
     *    http : URL is absolute, forced to http scheme
     *   https : URL is absolute, forced to https scheme
     * 
* @param array $options An array of options for generating the Resource URL. * @return string The URL for the resource. */ public function makeUrl($id, $context= '', $args= '', $scheme= -1, array $options= []) { $url= ''; if ($validid = intval($id)) { $id = $validid; if ($context == '' || $this->context->get('key') == $context) { $url= $this->context->makeUrl($id, $args, $scheme, $options); } if (empty($url) && ($context !== $this->context->get('key'))) { $ctx= null; if ($context == '') { /** @var PDOStatement $stmt */ if ($stmt = $this->prepare("SELECT context_key FROM " . $this->getTableName(modResource::class) . " WHERE id = :id")) { $stmt->bindValue(':id', $id); if ($contextKey = $this->getValue($stmt)) { $ctx = $this->getContext($contextKey); } } } else { $ctx = $this->getContext($context); } if ($ctx) { $url= $ctx->makeUrl($id, $args, 'full', $options); } } if (!empty($url) && $this->getOption('xhtml_urls', $options, false)) { $url= preg_replace("/&(?!amp;)/","&", $url); } } else { $this->log(modX::LOG_LEVEL_ERROR, '`' . $id . '` is not a valid integer and may not be passed to makeUrl()'); } return $url; } /** * Filter a string for use as a URL path segment. * * @param string $string The string to filter into a valid path segment. * @param array $options Optional filter setting overrides. * * @return string|null A valid path segment string or null if an error occurs. */ public function filterPathSegment($string, array $options = []) { return $this->call(modResource::class, 'filterPathSegment', [&$this, $string, $options]); } public function findResource($uri, $context = '') { $resourceId = false; if (empty($context) && isset($this->context)) $context = $this->context->get('key'); if (!empty($context) && (!empty($uri) || $uri === '0')) { $useAliasMap = (bool) $this->getOption('cache_alias_map', null, false); if ($useAliasMap) { if (isset($this->context) && $this->context->get('key') === $context && is_array($this->aliasMap) && array_key_exists($uri, $this->aliasMap)) { $resourceId = (int) $this->aliasMap[$uri]; } elseif ($ctx = $this->getContext($context)) { $useAliasMap = $ctx->getOption('cache_alias_map', false) && is_array($ctx->aliasMap) && array_key_exists($uri, $ctx->aliasMap); if ($useAliasMap && array_key_exists($uri, $ctx->aliasMap)) { $resourceId = (int) $ctx->aliasMap[$uri]; } } } if (!$resourceId && !$useAliasMap) { $query = $this->newQuery(modResource::class, ['context_key' => $context, 'uri' => $uri, 'deleted' => false] ); $query->select($this->getSelectColumns(modResource::class, '', '', ['id'])); $stmt = $query->prepare(); if ($stmt) { $value = $this->getValue($stmt); if ($value) { $resourceId = $value; } } } } return $resourceId; } /** * Send the user to a type-specific core error page and halt PHP execution. * * @param string $type The type of error to present. * @param array $options An array of options to provide for the error file. */ public function sendError($type = '', $options = []) { if (!is_string($type) || empty($type)) $type = $this->getOption('error_type', $options, 'unavailable'); while (ob_get_level() && @ob_end_clean()) {} if (!XPDO_CLI_MODE) { $errorPageTitle = $this->getOption('error_pagetitle', $options, 'Error 503: Service temporarily unavailable'); $errorMessage = $this->getOption('error_message', $options, '

Site temporarily unavailable.

'); $errorHeader = $this->getOption('error_header', $options, $_SERVER['SERVER_PROTOCOL'] . ' 503 Service Unavailable'); if (file_exists(MODX_CORE_PATH . "error/{$type}.include.php")) { @include(MODX_CORE_PATH . "error/{$type}.include.php"); } header($errorHeader); echo "{$errorPageTitle}{$errorMessage}"; @session_write_close(); } else { echo ucfirst($type) . "\n"; echo $this->getOption('error_message', $options, 'Service temporarily unavailable') . "\n"; } exit(); } /** * Sends a redirect to the specified URL using the specified options. * * Valid 'type' option values include: * REDIRECT_REFRESH Uses the header refresh method * REDIRECT_META Sends a a META HTTP-EQUIV="Refresh" tag to the output * REDIRECT_HEADER Uses the header location method * * REDIRECT_HEADER is the default. * * @param string $url The URL to redirect the client browser to. * @param array|boolean $options An array of options for the redirect OR * indicates if redirect attempts should be counted and limited to 3 (latter is deprecated * usage; use count_attempts in options array). * @param string $type The type of redirection to attempt (deprecated, use type in * options array). * @param string $responseCode The type of HTTP response code HEADER to send for the * redirect (deprecated, use responseCode in options array) */ public function sendRedirect($url, $options= false, $type= '', $responseCode = '') { if (!$this->getResponse()) { $this->log(modX::LOG_LEVEL_FATAL, "Could not load response class."); } if (!is_array($options)) { $options = ['count_attempts' => (bool) $options]; } if ($type) { $this->deprecated('2.0.5', 'Use type in options array instead.', 'sendRedirect method parameter $type'); $options['type'] = $type; $type = ''; } if ($responseCode) { $this->deprecated('2.0.5', 'Use responseCode in options array instead.', 'sendRedirect method parameter $responseCode'); $options['responseCode'] = $responseCode; $responseCode = ''; } $this->response->sendRedirect($url, $options, $type, $responseCode); } /** * Forwards the request to another resource without changing the URL. * * @param integer $id The resource identifier. * @param string $options An array of options for the process. * @param boolean $sendErrorPage Whether we should skip the sendErrorPage if the resource does not exist. */ public function sendForward($id, $options = null, $sendErrorPage = true) { if (!$this->getRequest()) { $this->log(modX::LOG_LEVEL_FATAL, "Could not load request class."); } $idInt = intval($id); if (is_string($options) && !empty($options)) { $options = ['response_code' => $options]; } elseif (!is_array($options)) { $options = []; } $this->elementCache = []; if ($idInt > 0) { $merge = array_key_exists('merge', $options) && !empty($options['merge']); $currentResource = []; if ($merge) { $excludes = array_merge( explode(',', $this->getOption('forward_merge_excludes', $options, 'type,published,class_key')), [ 'content' ,'pub_date' ,'unpub_date' ,'richtext' ,'_content' ,'_processed', ] ); if (!empty($this->resource->_fields)) { foreach ($this->resource->_fields as $fkey => $fval) { if (!in_array($fkey, $excludes)) { if (is_scalar($fval) && $fval !== '') { $currentResource[$fkey] = $fval; } elseif (is_array($fval) && count($fval) === 5 && $fval[1] !== '') { $currentResource[$fkey] = $fval; } } } } } $this->resource= $this->request->getResource('id', $idInt, ['forward' => true]); if ($this->resource) { if ($merge && !empty($currentResource)) { $this->resource->_fields = array_merge($this->resource->_fields, $currentResource); $this->elementCache = []; unset($currentResource); } $this->resourceIdentifier= $this->resource->get('id'); $this->resourceMethod= 'id'; if (isset($options['response_code']) && !empty($options['response_code'])) { header($options['response_code']); } $this->request->prepareResponse(); exit(); } elseif ($sendErrorPage) { $this->sendErrorPage(); } $options= array_merge( [ 'error_type' => '404' ,'error_header' => $this->getOption('error_page_header', $options,$_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found') ,'error_pagetitle' => $this->getOption('error_page_pagetitle', $options,'Error 404: Page not found') ,'error_message' => $this->getOption('error_page_message', $options,'

Page not found

The page you requested was not found.

'), ], $options ); } $this->sendError($id, $options); } /** * Send the user to a MODX virtual error page. * * @uses invokeEvent() The OnPageNotFound event is invoked before the error page is forwarded * to. * @param array $options An array of options to provide for the OnPageNotFound event and error * page. */ public function sendErrorPage($options = null) { if (!is_array($options)) $options = []; $options= array_merge( [ 'response_code' => $this->getOption('error_page_header', $options, $_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found') ,'error_type' => '404' ,'error_header' => $this->getOption('error_page_header', $options, $_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found') ,'error_pagetitle' => $this->getOption('error_page_pagetitle', $options, 'Error 404: Page not found') ,'error_message' => $this->getOption('error_page_message', $options, '

Page not found

The page you requested was not found.

'), ], $options ); $this->invokeEvent('OnPageNotFound', $options); $this->sendForward($this->getOption('error_page', $options, $this->getOption('site_start')), $options, false); } /** * Send the user to the MODX unauthorized page. * * @uses invokeEvent() The OnPageUnauthorized event is invoked before the unauthorized page is * forwarded to. * @param array $options An array of options to provide for the OnPageUnauthorized * event and unauthorized page. */ public function sendUnauthorizedPage($options = null) { if (!is_array($options)) $options = []; $options= array_merge( [ 'response_code' => $this->getOption('unauthorized_page_header' ,$options ,$_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized') ,'error_type' => '401' ,'error_header' => $this->getOption('unauthorized_page_header', $options,$_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized') ,'error_pagetitle' => $this->getOption('unauthorized_page_pagetitle',$options, 'Error 401: Unauthorized') ,'error_message' => $this->getOption('unauthorized_page_message', $options,'

Unauthorized

You are not authorized to view the requested content.

'), ], $options ); $this->invokeEvent('OnPageUnauthorized', $options); $this->sendForward($this->getOption('unauthorized_page', $options, $this->getOption('site_start')), $options); } /** * Get the current authenticated User and assign it to the modX instance. * * @param string $contextKey An optional context to get the user from. * @param boolean $forceLoadSettings If set to true, will load settings * regardless of whether the user has an authenticated context or not. * @return modUser The user object authenticated for the request. */ public function getUser($contextKey= '',$forceLoadSettings = false) { if ($contextKey == '') { if ($this->context !== null) { $contextKey= $this->context->get('key'); } } if ($this->user === null || !is_object($this->user)) { $this->user= $this->getAuthenticatedUser($contextKey); if ($contextKey !== 'mgr' && !$this->user) { $this->user= $this->getAuthenticatedUser('mgr'); } } if ($this->user !== null && is_object($this->user)) { if ($this->user->hasSessionContext($contextKey) || $forceLoadSettings) { if (!$forceLoadSettings && isset ($_SESSION["modx.{$contextKey}.user.config"])) { $this->_userConfig= $_SESSION["modx.{$contextKey}.user.config"]; } else { $this->_userConfig= []; $settings= $this->user->getSettings(); if (is_array($settings) && !empty ($settings)) { foreach ($settings as $k => $v) { $matches= []; if (preg_match_all('~\{(.*?)\}~', $v, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { if (isset($this->_userConfig["{$match[1]}"])) { $matchValue= $this->_userConfig["{$match[1]}"]; } elseif (isset($this->config["{$match[1]}"])) { $matchValue= $this->config["{$match[1]}"]; } else { $matchValue= ''; } $v= str_replace($match[0], $matchValue, $v); } } $this->_userConfig[$k]= $v; } } $_SESSION["modx.{$contextKey}.user.config"]= $this->_userConfig; } if (is_array($this->_userConfig) && !empty($this->_userConfig)) { $this->config= array_merge($this->config, $this->_userConfig); } } } else { $this->user = $this->newObject(modUser::class); $this->user->fromArray( [ 'id' => 0, 'username' => $this->getOption('default_username','','(anonymous)',true), ], '', true); } ksort($this->config); $this->toPlaceholders($this->user->get(['id','username']), 'modx.user'); return $this->user; } /** * Gets the user authenticated in the specified context. * * @param string $contextKey Optional context key; uses current context by default. * @return modUser|null The user object that is authenticated in the specified context, * or null if no user is authenticated. */ public function getAuthenticatedUser($contextKey= '') { $user= null; if ($contextKey == '') { if ($this->context !== null) { $contextKey= $this->context->get('key'); } } if ($contextKey && isset ($_SESSION['modx.user.contextTokens'][$contextKey])) { /** @var modUser $user */ $user= $this->getObject(modUser::class, intval($_SESSION['modx.user.contextTokens'][$contextKey]), true); if ($user) { $user->getSessionContexts(); } } return $user; } /** * Checks to see if the user has a session in the specified context. * * @param string $sessionContext The context to test for a session key in. * @return boolean True if the user is valid in the context specified. */ public function checkSession($sessionContext= 'web') { $hasSession = false; if ($this->user !== null) { $hasSession = $this->user->hasSessionContext($sessionContext); } return $hasSession; } /** * Gets the modX core version data. * * @return array The version data loaded from the config version file. */ public function getVersionData() { if ($this->version === null) { $this->version = @ include MODX_CORE_PATH . "docs/version.inc.php"; } return $this->version; } /** * Reload the config settings. * * @return array An associative array of configuration key/values */ public function reloadConfig() { $this->getCacheManager(); $this->cacheManager->refresh(); if (!$this->_loadConfig()) { $this->log(modX::LOG_LEVEL_ERROR, 'Could not reload core MODX configuration!'); } return $this->config; } /** * Get the configuration for the site. * * @return array An associate array of configuration key/values */ public function getConfig() { if (!$this->_initialized || !is_array($this->config) || empty ($this->config)) { if (!isset ($this->config['base_url'])) $this->config['base_url']= MODX_BASE_URL; if (!isset ($this->config['base_path'])) $this->config['base_path']= MODX_BASE_PATH; if (!isset ($this->config['core_path'])) $this->config['core_path']= MODX_CORE_PATH; if (!isset ($this->config['url_scheme'])) $this->config['url_scheme']= MODX_URL_SCHEME; if (!isset ($this->config['http_host'])) $this->config['http_host']= MODX_HTTP_HOST; if (!isset ($this->config['site_url'])) $this->config['site_url']= MODX_SITE_URL; if (!isset ($this->config['manager_path'])) $this->config['manager_path']= MODX_MANAGER_PATH; if (!isset ($this->config['manager_url'])) $this->config['manager_url']= MODX_MANAGER_URL; if (!isset ($this->config['assets_path'])) $this->config['assets_path']= MODX_ASSETS_PATH; if (!isset ($this->config['assets_url'])) $this->config['assets_url']= MODX_ASSETS_URL; if (!isset ($this->config['connectors_path'])) $this->config['connectors_path']= MODX_CONNECTORS_PATH; if (!isset ($this->config['connectors_url'])) $this->config['connectors_url']= MODX_CONNECTORS_URL; if (!isset ($this->config['connector_url'])) $this->config['connector_url']= MODX_CONNECTORS_URL . 'index.php'; if (!isset ($this->config['processors_path'])) $this->config['processors_path']= MODX_PROCESSORS_PATH; if (!isset ($this->config['request_param_id'])) $this->config['request_param_id']= 'id'; if (!isset ($this->config['request_param_alias'])) $this->config['request_param_alias']= 'q'; if (!isset ($this->config['https_port'])) $this->config['https_port']= isset($GLOBALS['https_port']) ? $GLOBALS['https_port'] : 443; if (!isset ($this->config['error_handler_class'])) $this->config['error_handler_class']= modErrorHandler::class; if (!isset ($this->config['server_port'])) $this->config['server_port']= isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : ''; $this->_config= $this->config; if (!$this->_loadConfig()) { $this->log(modX::LOG_LEVEL_FATAL, "Could not load core MODX configuration!"); return null; } } return $this->config; } /** * Initialize, cleanse, and process a request made to a modX site. * * @return mixed The result of the request handler. */ public function handleRequest() { if ($this->getRequest()) { return $this->request->handleRequest(); } return ''; } /** * Attempt to load the request handler class, if not already loaded. * * @access public * @param string $class The class name of the response class to load. Defaults to * modRequest; is ignored if the Setting "modRequest.class" is set. * @param string $path The absolute path by which to load the response class from. * Defaults to the current MODX model path. * @return boolean Returns true if a valid request handler object was * loaded on this or any previous call to the function, false otherwise. */ public function getRequest($class= modRequest::class, $path= '') { $className = $this->getOption('modRequest.class', $this->config, $class); if ($this->request === null || !($this->request instanceof $className)) { $this->request= new $className($this); } return is_object($this->request) && $this->request instanceof $className; } /** * Attempt to load the response handler class, if not already loaded. * * @access public * @param string $class The class name of the response class to load. Defaults to * modResponse; is ignored if the Setting "modResponse.class" is set. * @param string $path The absolute path by which to load the response class from. * Defaults to the current MODX model path. * @return boolean Returns true if a valid response handler object was * loaded on this or any previous call to the function, false otherwise. */ public function getResponse($class= modResponse::class, $path= '') { $className = $this->getOption('modResponse.class', $this->config, $class); if ($this->response === null || !($this->response instanceof $className)) { $this->response = new $className($this); } return $this->response instanceof $className; } /** * Register CSS to be injected inside the HEAD tag of a resource. * * @param string $src The CSS to be injected before the closing HEAD tag in * an HTML response. * @param string $media all, aural, braille, embossed, handheld, print, projection, screen, tty, tv * @return void */ public function regClientCSS($src, $media = null) { if (isset ($this->loadedjscripts[$src]) && $this->loadedjscripts[$src]) { return; } $this->loadedjscripts[$src]= true; if (strpos(strtolower($src), "sjscripts[count($this->sjscripts)]= $src; } else { if (!empty($media)) { $media = ' media="' . $media .'"'; } $this->sjscripts[count($this->sjscripts)]= ''; } } /** * Register JavaScript to be injected inside the HEAD tag of a resource. * * @param string $src The JavaScript to be injected before the closing HEAD * tag of an HTML response. * @param boolean $plaintext Optional param to treat the $src as plaintext * rather than assuming it is JavaScript. * @return void */ public function regClientStartupScript($src, $plaintext= false) { if (!empty ($src) && !array_key_exists($src, $this->loadedjscripts)) { if (isset ($this->loadedjscripts[$src])) return; $this->loadedjscripts[$src]= true; if ($plaintext == true) { $this->sjscripts[count($this->sjscripts)]= $src; } elseif (strpos(strtolower($src), "sjscripts[count($this->sjscripts)]= $src; } else { $this->sjscripts[count($this->sjscripts)]= ''; } } } /** * Register JavaScript to be injected before the closing BODY tag. * * @param string $src The JavaScript to be injected before the closing BODY * tag in an HTML response. * @param boolean $plaintext Optional param to treat the $src as plaintext * rather than assuming it is JavaScript. * @return void */ public function regClientScript($src, $plaintext= false) { if (isset ($this->loadedjscripts[$src])) return; $this->loadedjscripts[$src]= true; if ($plaintext == true) { $this->jscripts[count($this->jscripts)]= $src; } elseif (strpos(strtolower($src), "jscripts[count($this->jscripts)]= $src; } else { $this->jscripts[count($this->jscripts)]= ''; } } /** * Register HTML to be injected before the closing HEAD tag. * * @param string $html The HTML to be injected. */ public function regClientStartupHTMLBlock($html) { $this->regClientStartupScript($html, true); } /** * Register HTML to be injected before the closing BODY tag. * * @param string $html The HTML to be injected. */ public function regClientHTMLBlock($html) { $this->regClientScript($html, true); } /** * Returns all registered JavaScripts. * * @access public * @return string The parsed HTML of the client scripts. */ public function getRegisteredClientScripts() { $string= ''; if (is_array($this->jscripts)) { $string= implode("\n",$this->jscripts); } return $string; } /** * Returns all registered startup CSS, JavaScript, or HTML blocks. * * @access public * @return string The parsed HTML of the startup scripts. */ public function getRegisteredClientStartupScripts() { $string= ''; if (is_array ($this->sjscripts)) { $string= implode("\n", $this->sjscripts); } return $string; } /** * Invokes a specified Event with an optional array of parameters. * * @todo refactor this completely, yuck!! * * @access public * @param string $eventName Name of an event to invoke. * @param array $params Optional params provided to the elements registered with an event. * @return bool|array */ public function invokeEvent($eventName, array $params= []) { if (!$eventName) return false; if ($this->eventMap === null && $this->context instanceof modContext) $this->_initEventMap($this->context->get('key')); if (!isset ($this->eventMap[$eventName])) { //$this->log(modX::LOG_LEVEL_DEBUG,'System event '.$eventName.' was executed but does not exist.'); return false; } $results= []; if (count($this->eventMap[$eventName])) { $this->event= new modSystemEvent(); foreach ($this->eventMap[$eventName] as $pluginId => $pluginPropset) { /** @var modPlugin $plugin */ $plugin= null; $this->Event = clone $this->event; $this->event->resetEventObject(); $this->event->name= $eventName; if (isset ($this->pluginCache[$pluginId])) { $plugin= $this->newObject(modPlugin::class); $plugin->fromArray($this->pluginCache[$pluginId], '', true, true); $plugin->_processed = false; if ($plugin->get('disabled')) { $plugin= null; } } else { $plugin= $this->getObject(modPlugin::class, ['id' => intval($pluginId), 'disabled' => '0'], true); } if ($plugin && !$plugin->get('disabled')) { $this->event->plugin =& $plugin; $this->event->activated= true; $this->event->activePlugin= $plugin->get('name'); $this->event->propertySet= (($pspos = strpos($pluginPropset, ':')) >= 1) ? substr($pluginPropset, $pspos + 1) : ''; /* merge in plugin properties */ $eventParams = array_merge($plugin->getProperties(),$params); $msg= $plugin->process($eventParams); $results[]= $this->event->_output; if ($msg && is_string($msg)) { $this->log(modX::LOG_LEVEL_ERROR, '[' . $this->event->name . ']' . $msg); } elseif ($msg === false) { $this->log(modX::LOG_LEVEL_ERROR, '[' . $this->event->name . '] Plugin ' . $plugin->name . ' failed!'); } $this->event->plugin = null; $this->event->activePlugin= ''; $this->event->propertySet= ''; if (!$this->event->isPropagatable()) { break; } } } } return $results; } /** * Loads and runs a specific processor. * * @param string $action The processor to run, eg: context/update, or the processor class name (\MODX\Revolution\Processors\Context\Update::class) * @param array $scriptProperties Optional. An array of parameters to pass to the processor. * @param array $options Optional. An array of options for running the processor, only supports "processors_path" to set a custom path * * @return ProcessorResponse|mixed The result of the processor. Typically a ProcessorResponse object, but may be different for specific processors. */ public function runProcessor($action = '', $scriptProperties = [], $options = []) { $result = null; // Make sure the required services are loaded before initialising a processor if (!$this->lexicon) { if (!$this->services->has('lexicon')) { $this->services->add('lexicon', new modLexicon($this)); } $this->lexicon = $this->services->get('lexicon'); } if (!$this->error) { if (!$this->services->has('error')) { $this->services->add('error', new modError($this)); } $this->error = $this->services->get('error'); } // First check if the processor can be found directly as a class name provided to $action // We're limiting to subclasses of Processor to prevent instantiating arbitrary classes if (static::isProcessorClass($action)) { /** @var Processor $processor */ $processor = $action::getInstance($this, $action, $scriptProperties); return $processor->run(); } // Check if we're dealing with an action like "context/update", which we transform into the 3.0+ class name $legacyAction = 'MODX\\Revolution\\Processors\\' . implode('\\', array_map('ucfirst', explode('/', $action))); // Special case for TVs, for which the directory is named differently from how it was called in the past if (strpos($legacyAction, '\\Tv\\') !== false) { $legacyAction = str_replace('\\Tv\\', '\\TemplateVar\\', $legacyAction); } if (static::isProcessorClass($legacyAction)) { /** @var Processor $processor */ $processor = $legacyAction::getInstance($this, $legacyAction, $scriptProperties); return $processor->run(); } // If we don't have a processor yet, we're dealing with a custom processor. See if we have been provided a path to look in. $processorsPath = isset($options['processors_path']) && !empty($options['processors_path']) ? $options['processors_path'] : $this->config['processors_path']; // Prevent path traversal through the action $action = preg_replace('/[\.]{2,}/', '', htmlspecialchars($action)); // Find a matching processor file $processorFile = $processorsPath . ltrim($action . '.class.php','/'); if (!file_exists($processorFile)) { $this->log(modX::LOG_LEVEL_ERROR, "Unable to load processor for action \"{$action}\", it does not exist as an autoloadable class that extends \MODX\Revolution\Processors\Processor, and also not as a file in \"{$processorFile}\""); return new ProcessorResponse($this, [ 'success' => false, 'message' => 'Requested processor not found', ]); } // Check if we already know the name for the class (meaning we've loaded it before in this request) if (array_key_exists($processorFile,$this->processors)) { $className = $this->processors[$processorFile]; } // Load the file. The file is expected to return (as a string) the name of its class. else { $className = include_once $processorFile; // include_once returns 1 if there was no return value in the file, or true if it was already loaded before // In both cases we can guess the class name based on the provided action if ($className === 1 || $className === true) { $section = explode('/', $action); $pieces = []; foreach ($section as $k) { $pieces[] = ucfirst(str_replace(['.', '_', '-'], '', $k)); } // This is an older construct, introduced in MODX 2.2. It's better to be explicit and return the name of your class in the processor $className = 'mod' . implode('', $pieces) . 'Processor'; } $this->processors[$processorFile] = $className; } if (static::isProcessorClass($className)) { /** @var Processor $processor */ $processor = $className::getInstance($this, $className, $scriptProperties); $processor->setPath($processorFile); return $processor->run(); } $this->log(modX::LOG_LEVEL_ERROR, "Unable to load processor for action \"{$action}\". Its file in \"{$processorFile}\" exists, but does not return the processor class name and the class name could not be automatically inferred from the action."); return new ProcessorResponse($this, [ 'success' => false, 'message' => 'Requested processor is invalid.', ]); } /** * @param string $className * @return bool */ private static function isProcessorClass(string $className): bool { return class_exists($className) && is_subclass_of($className, Processor::class, true); } /** * Returns the current user ID, for the current or specified context. * * @param string $context The key of a valid modContext so you can retrieve * the current user ID from a different context than the current. * @return integer The ID of the current user. */ public function getLoginUserID($context= '') { $userId = 0; if (empty($context) && $this->context instanceof modContext && $this->user instanceof modUser) { if ($this->user->hasSessionContext($this->context->get('key'))) { $userId = $this->user->get('id'); } } else { $user = $this->getAuthenticatedUser($context); if ($user instanceof modUser) { $userId = $user->get('id'); } } return $userId; } /** * Returns the current user name, for the current or specified context. * * @param string $context The key of a valid modContext so you can retrieve * the username from a different context than the current. * @return string The username of the current user. */ public function getLoginUserName($context= '') { $userName = ''; if (empty($context) && $this->context instanceof modContext && $this->user instanceof modUser) { if ($this->user->hasSessionContext($this->context->get('key'))) { $userName = $this->user->get('username'); } } else { $user = $this->getAuthenticatedUser($context); if ($user instanceof modUser) { $userName = $user->get('username'); } } return $userName; } /** * Returns whether modX instance has been initialized or not. * * @access public * @return boolean */ public function isInitialized() { return $this->_initialized; } /** * Legacy fatal error message. * * @deprecated * @param string $msg * @param string $query * @param bool $is_error * @param string $nr * @param string $file * @param string $source * @param string $text * @param string $line */ public function messageQuit($msg='unspecified error', $query='', $is_error=true, $nr='', $file='', $source='', $text='', $line='') { $this->deprecated('2.2.0', 'Use modX::log with modX::LOG_LEVEL_FATAL instead.'); $this->log(modX::LOG_LEVEL_FATAL, 'msg: ' . $msg . "\n" . 'query: ' . $query . "\n" . 'nr: ' . $nr . "\n" . 'file: ' . $file . "\n" . 'source: ' . $source . "\n" . 'text: ' . $text . "\n" . 'line: ' . $line . "\n"); } /** * Process and return the output from a PHP snippet by name. * * @param string $snippetName The name of the snippet. * @param array $params An associative array of properties to pass to the * snippet. * @return string The processed output of the snippet. */ public function runSnippet($snippetName, array $params= []) { $output= ''; if ($this->getParser()) { $snippet= $this->parser->getElement(modSnippet::class, $snippetName); if ($snippet instanceof modSnippet) { $snippet->setCacheable(false); $output= $snippet->process($params); } } return $output; } /** * Process and return the output from a Chunk by name. * * @param string $chunkName The name of the chunk. * @param array $properties An associative array of properties to process * the Chunk with, treated as placeholders within the scope of the Element. * @return string The processed output of the Chunk. */ public function getChunk($chunkName, array $properties= []) { $output= ''; if ($this->getParser()) { $chunk= $this->parser->getElement(modChunk::class, $chunkName); if ($chunk instanceof modChunk) { $chunk->setCacheable(false); $output= $chunk->process($properties); } } return $output; } /** * Parse a chunk using an associative array of replacement variables. * * @param string $chunkName The name of the chunk. * @param array $chunkArr An array of properties to replace in the chunk. * @param string $prefix The placeholder prefix, defaults to [[+. * @param string $suffix The placeholder suffix, defaults to ]]. * @return string The processed chunk with the placeholders replaced. * @deprecated Prefer using $modx->getChunk, to be removed in MODX 4.0. */ public function parseChunk($chunkName, $chunkArr, $prefix='[[+', $suffix=']]') { $this->deprecated('3.0.0', 'Use $modx->getChunk instead'); $chunk= $this->getChunk($chunkName); if (!empty($chunk) || $chunk === '0') { if(is_array($chunkArr)) { foreach ($chunkArr as $key => $value) { $chunk= str_replace($prefix.$key.$suffix, $value, $chunk); } } } return $chunk; } /** * Strip unwanted HTML and PHP tags and supplied patterns from content. * * @see modX::$sanitizePatterns * @param string $html The string to strip * @param string $allowed An array of allowed HTML tags * @param array $patterns An array of patterns to sanitize with; otherwise will use modX::$sanitizePatterns * @param int $depth The depth in which the parser will strip given the patterns specified * @return boolean True if anything was stripped */ public function stripTags($html, $allowed= '', $patterns= [], $depth= 10) { $stripped= strip_tags($html, $allowed); if (is_array($patterns)) { if (empty($patterns)) { $patterns = $this->sanitizePatterns; } foreach ($patterns as $pattern) { $depth = ((int) $depth ? (int) $depth : 10); $iteration = 1; while ($iteration <= $depth && preg_match($pattern, $stripped)) { $stripped= preg_replace($pattern, '', $stripped); $iteration++; } } } return $stripped; } /** * Returns true if user has the specified policy permission. * * @param string $pm Permission key to check. * @return boolean */ public function hasPermission($pm) { $state = $this->context->checkPolicy($pm); return $state; } /** * Logs a manager action. * * @param string $action The action to pull from the lexicon module. * @param string $class_key The class key that the action is being performed on. * @param mixed $item The primary key id or array of keys to grab the object with. * @param int|null $userId * @return modManagerLog The newly created modManagerLog object. */ public function logManagerAction($action, $class_key, $item, $userId = null) { if($userId === null) { if ($this->user instanceof modUser) { $userId = $this->user->get('id'); } } /** @var modManagerLog $ml */ $ml = $this->newObject(modManagerLog::class); $ml->set('user', (int) $userId); $ml->set('occurred', date('Y-m-d H:i:s')); $ml->set('action', empty($action) ? 'unknown' : $action); $ml->set('classKey', empty($class_key) ? '' : $class_key); $ml->set('item', empty($item) ? 'unknown' : $item); if (!$ml->save()) { $this->log(modX::LOG_LEVEL_ERROR, $this->lexicon('manager_log_err_save')); return null; } return $ml; } /** * Remove an event from the eventMap so it will not be invoked. * * @param string $event * @param integer $pluginId Plugin identifier to remove from the eventMap for the specified event. * @return boolean false if the event parameter is not specified or is not * present in the eventMap. */ public function removeEventListener($event, $pluginId = 0) { $removed = false; if (!empty($event) && isset($this->eventMap[$event])) { if (intval($pluginId)) { unset ($this->eventMap[$event][$pluginId]); } else { unset ($this->eventMap[$event]); } $removed = true; } return $removed; } /** * Remove all registered events for the current request. */ public function removeAllEventListener() { unset ($this->eventMap); $this->eventMap= []; } /** * Add a plugin to the eventMap within the current execution cycle. * * @param string $event Name of the event. * @param integer $pluginId Plugin identifier to add to the event. * @param string $propertySetName The name of property set bound to the plugin * @return boolean true if the event is successfully added, otherwise false. */ public function addEventListener($event, $pluginId, $propertySetName = '') { $added = false; $pluginId = intval($pluginId); if ($event && $pluginId) { if (!isset($this->eventMap[$event]) || empty ($this->eventMap[$event])) { $this->eventMap[$event]= []; } $this->eventMap[$event][$pluginId]= $pluginId . (!empty($propertySetName) ? ':' . $propertySetName : ''); $added = true; } return $added; } /** * Switches the primary Context for the modX instance. * * Be aware that switching contexts does not allow custom session handling * classes to be loaded. The gateway defines the session handling that is * applied to a single request. To create a context with a custom session * handler you must create a unique context gateway that initializes that * context directly. * * @param string $contextKey The key of the context to switch to. * @param boolean $reload Set to true to force the context data to be regenerated * before being switched to. * @return boolean True if the switch was successful, otherwise false. */ public function switchContext($contextKey, $reload = false) { $switched= false; if ($this->context->key != $contextKey) { $switched= $this->_initContext($contextKey, $reload); if ($switched) { if (is_array($this->config)) { $this->setPlaceholders($this->config, '+'); } } } return $switched; } /** * Retrieve a context by name without initializing it. * * Within a request, contexts retrieved using this function will cache the * context data into the modX::$contexts array to avoid loading the same * context multiple times. * * @access public * @param string $contextKey The context to retrieve. * @return modContext A modContext object retrieved from cache or * database. */ public function getContext($contextKey) { if (!isset($this->contexts[$contextKey])) { $this->contexts[$contextKey]= $this->getObject(modContext::class, ['key' => $contextKey]); if ($this->contexts[$contextKey]) { $this->contexts[$contextKey]->prepare(); } } return $this->contexts[$contextKey]; } /** * Gets a map of events and registered plugins for the specified context. * * Service #s: * 1 - Parser Service Events * 2 - Manager Access Events * 3 - Web Access Service Events * 4 - Cache Service Events * 5 - Template Service Events * 6 - User Defined Events * * @param string $contextKey Context identifier. * @return array A map of events and registered plugins for each. */ public function getEventMap($contextKey) { $eventElementMap= []; if ($contextKey) { switch ($contextKey) { case 'mgr': /* dont load Web Access Service Events */ $service= "Event.service IN (1,2,4,5,6) AND"; break; default: /* dont load Manager Access Events */ $service= "Event.service IN (1,3,4,5,6) AND"; } $pluginEventTbl= $this->getTableName(modPluginEvent::class); $eventTbl= $this->getTableName(modEvent::class); $pluginTbl= $this->getTableName(modPlugin::class); $propsetTbl= $this->getTableName(modPropertySet::class); $sql= " SELECT Event.name AS event, PluginEvent.pluginid, PropertySet.name AS propertyset FROM {$pluginEventTbl} PluginEvent INNER JOIN {$pluginTbl} Plugin ON Plugin.id = PluginEvent.pluginid AND Plugin.disabled = 0 INNER JOIN {$eventTbl} Event ON {$service} Event.name = PluginEvent.event LEFT JOIN {$propsetTbl} PropertySet ON PluginEvent.propertyset = PropertySet.id ORDER BY Event.name, PluginEvent.priority ASC "; $stmt= $this->prepare($sql); if ($stmt && $stmt->execute()) { while ($ee = $stmt->fetch(PDO::FETCH_ASSOC)) { $eventElementMap[$ee['event']][(string) $ee['pluginid']]= $ee['pluginid'] . (!empty($ee['propertyset']) ? ':' . $ee['propertyset'] : ''); } } } return $eventElementMap; } /** * Checks for locking on a page. * * @param integer $id Id of the user checking for a lock. * @param string $action The action identifying what is locked. * @param string $type Message indicating the kind of lock being checked. * @return string|boolean If locked, will return a locked message */ public function checkForLocks($id,$action,$type) { $msg= false; $id= intval($id); if (!$id) $id= $this->getLoginUserID(); if ($au = $this->getObject(modActiveUser::class, ['action' => $action, 'internalKey:!=' => $id])) { $msg = $this->lexicon('lock_msg', [ 'name' => $au->get('username'), 'object' => $type, ] ); } return $msg; } /** * Grabs a processed lexicon string. * * @access public * @param string $key * @param array $params * @param string $language * @return null|string The translated string, or null if none is set */ public function lexicon($key,$params = [],$language = '') { $language = !empty($language) ? $language : $this->getOption('cultureKey',null,'en'); if ($this->lexicon) { return $this->lexicon->process($key,$params,$language); } else { $this->log(modX::LOG_LEVEL_ERROR,'Culture not initialized; cannot use lexicon.'); } return null; } /** * Returns the state of the SESSION being used by modX. * * The possible values for session state are: * * modX::SESSION_STATE_UNINITIALIZED * modX::SESSION_STATE_UNAVAILABLE * modX::SESSION_STATE_EXTERNAL * modX::SESSION_STATE_INITIALIZED * * @return integer Returns an integer representing the session state. */ public function getSessionState() { if ($this->_sessionState !== modX::SESSION_STATE_INITIALIZED) { if (XPDO_CLI_MODE || headers_sent()) { $this->_sessionState = modX::SESSION_STATE_UNAVAILABLE; } elseif (session_status() === PHP_SESSION_ACTIVE) { $this->_sessionState = modX::SESSION_STATE_EXTERNAL; } } return $this->_sessionState; } /** * Executed before parser processing of an element. */ public function beforeProcessing() {} /** * Executed before the response is rendered. */ public function beforeRender() {} /** * Executed before the handleRequest function. */ public function beforeRequest() { unset($this->placeholders['+username'],$this->placeholders['+password'],$this->placeholders['+dbname'],$this->placeholders['+host']); } /** * Determines the current site_status. * * @return boolean True if the site is online or the user has a valid * user session in the 'mgr' context; false otherwise. */ public function checkSiteStatus() { $status = false; if ($this->config['site_status'] == '1' || ($this->getSessionState() === modX::SESSION_STATE_INITIALIZED && $this->hasPermission('view_offline'))) { $status = true; } return $status; } /** * Add an extension package to MODX * * @param string $name * @param string $path * @param array $options * @return boolean */ public function addExtensionPackage($name,$path,array $options = []) { $extPackages = $this->getOption('extension_packages'); $extPackages = !empty($extPackages) ? $extPackages : []; $extPackages = is_array($extPackages) ? $extPackages : $this->fromJSON($extPackages); $extPackages[$name] = $options; $extPackages['path'] = $path; /** @var modSystemSetting $setting */ $setting = $this->getObject(modSystemSetting::class, [ 'key' => 'extension_packages', ] ); if (empty($setting)) { $setting = $this->newObject(modSystemSetting::class); $setting->set('key','extension_packages'); $setting->set('namespace','core'); $setting->set('xtype','textfield'); $setting->set('area','system'); } $value = $setting->get('value'); $value = is_array($value) ? $value : $this->fromJSON($value); if (empty($value)) { $value = []; $value[$name] = $options; $value[$name]['path'] = $path; $value = '['.$this->toJSON($value).']'; } else { $found = false; foreach ($value as $k => $v) { foreach ($v as $kk => $vv) { if ($kk == $name) { $found = true; } } } if (!$found) { $extPack[$name] = $options; $extPack[$name]['path'] = $path; $value[] = $extPack; } $value = $this->toJSON($value); } $value = str_replace('\\','',$value); $setting->set('value',$value); return $setting->save(); } /** * Remove an extension package from MODX * * @param string $name * @return boolean */ public function removeExtensionPackage($name) { /** @var modSystemSetting $setting */ $setting = $this->getObject(modSystemSetting::class, [ 'key' => 'extension_packages', ] ); if (!$setting) { return false; } $value = $setting->get('value'); $value = is_array($value) ? $value : $this->fromJSON($value); $found = false; foreach ($value as $idx => $extPack) { foreach ($extPack as $key => $opt) { if ($key == $name) { unset($value[$idx]); $found = true; } } } $removed = false; if ($found) { $value = $this->toJSON($value); $value = str_replace('\\','',$value); $setting->set('value',$value); $removed = $setting->save(); } return $removed; } /** * Reload data for a specified Context, without switching to it. * * Note that the Context will be loaded even if it is not already. * * @param string $key The key of the Context to (re)load. * @return boolean True if the Context was (re)loaded successfully; false otherwise. */ public function reloadContext($key = null) { $reloaded = false; if ($this->context instanceof modContext) { if (empty($key)) { $key = $this->context->get('key'); } if ($key === $this->context->get('key')) { $reloaded = $this->_initContext($key, true); if ($reloaded && is_array($this->config)) { $this->setPlaceholders($this->config, '+'); } } else { if (!array_key_exists($key, $this->contexts) || !($this->contexts[$key] instanceof modContext)) { $this->contexts[$key] = $this->newObject(modContext::class); $this->contexts[$key]->_fields['key']= $key; } $reloaded = $this->contexts[$key]->prepare(true); } } elseif (!empty($key) && (!array_key_exists($key, $this->contexts) || !($this->contexts[$key] instanceof modContext))) { $this->contexts[$key] = $this->newObject(modContext::class); $this->contexts[$key]->_fields['key']= $key; $reloaded = $this->contexts[$key]->prepare(true); } return $reloaded; } /** * Start a PHP Session if one is not already active. * * This is the single entry point for starting the modX session. It is used when * initializing modX (subject to the anonymous_sessions setting) and on demand * whenever a session is required, e.g. by modUser::addSessionContext() at login. * * @return bool Returns true if a session is successfully or already started, false otherwise. */ public function startSession() { if (session_status() === PHP_SESSION_ACTIVE) { if ($this->_sessionState !== modX::SESSION_STATE_INITIALIZED) { $this->_sessionState = modX::SESSION_STATE_EXTERNAL; } return true; } if (XPDO_CLI_MODE) { /* CLI scripts may provide their own $_SESSION array */ return isset($_SESSION); } $file = $line = null; if (headers_sent($file, $line)) { $msg = "Unable to start a session; output already started at {$file}:{$line}"; $this->log(modX::LOG_LEVEL_ERROR, $msg, '', __METHOD__, __FILE__, __LINE__); return false; } /* $_SESSION may have been written to before a session was active; session_start() replaces it */ $pending = isset($_SESSION) && is_array($_SESSION) ? $_SESSION : []; if (!session_start()) { if ($this->_sessionState !== modX::SESSION_STATE_INITIALIZED) { $this->_sessionState = modX::SESSION_STATE_UNAVAILABLE; } return false; } if (!empty($pending)) { $_SESSION = array_merge($pending, $_SESSION); } $this->_sessionState = modX::SESSION_STATE_INITIALIZED; return true; } /** * Marks the calling function as deprecated, sending a message into the error log. * * This automatically determines where the deprecated method was called from, and * includes that in the log message. * * @param string $since The version the function was marked as deprecated * @param string $recommendation A description or recommendation on what to replace a method with * @param string $deprecatedDef Can be used to override the definition (i.e. function name) for the log; useful if not a specific method but an entire entity is deprecated. */ public function deprecated($since, $recommendation = '', $deprecatedDef = '') { if (!$this->getOption('log_deprecated', null, true)) { return; } // At the end of the request, save deprecations with their callers in one go register_shutdown_function(function () { foreach ($this->_deprecations as $deprecation) { $deprecation->save(); } }); // We use the trace to identify both the method that is deprecated, and the caller $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $deprecatedMethod = isset($trace[1]) ? $trace[1] : []; $caller = isset($trace[2]) ? $trace[2] : []; // Format the deprecated function definition with the class, if it has one if ($deprecatedDef === '') { $deprecatedDef = isset($deprecatedMethod['class']) ? $deprecatedMethod['class'] . '::' . $deprecatedMethod['function'] : $deprecatedMethod['function']; } $deprecation = $this->_getDeprecatedMethod($since, $deprecatedDef, $recommendation); $deprecation->addCaller($caller['class'] ?? '', $caller['function'] ?? '', $deprecatedMethod['file'], $deprecatedMethod['line']); } /** * * Gets an (in-memory cached) modDeprecatedMethod instance. * @param $since * @param $callerDef * @param $recommendation * @return modDeprecatedMethod */ private function _getDeprecatedMethod($since, $callerDef, $recommendation): modDeprecatedMethod { if (array_key_exists($callerDef, $this->_deprecations)) { return $this->_deprecations[$callerDef]; } /** @var modDeprecatedMethod $deprecation */ $deprecation = $this->getObject(modDeprecatedMethod::class, [ 'definition' => $callerDef, ]); if (!$deprecation) { $deprecation = $this->newObject(modDeprecatedMethod::class); $deprecation->set('definition', $callerDef); } // Exact messages may change from time-to-time, keep updated $deprecation->set('recommendation', $recommendation); $deprecation->set('since', $since); // We don't save it here - we do that at the end of the request $this->_deprecations[$callerDef] = $deprecation; return $deprecation; } /** * Load a class by fully qualified name. * * As of MODX 3.0, this is no longer necessary and deprecated. Instead of using loadClass, use the PSR-4 class * references that are available through the autoloader. * * @param string $fqn * @param string $path * @param bool $ignorePkg * @param bool $transient * @return bool|string * @deprecated Since 3.0 */ public function loadClass($fqn, $path = '', $ignorePkg = false, $transient = false) { if (strpos($fqn, '\\') !== false && class_exists($fqn)) { return $fqn; } if (empty($path)) { $class = $fqn; if (strpos($fqn, '.') !== false) { $tmp = explode('.', $fqn); $class = array_pop($tmp); if ($tmp[0] === 'modx') { array_shift($tmp); } if (count($tmp) > 0) { $class = implode('\\', array_map('ucfirst', $tmp)) . '\\' . $class; } } $class = 'MODX\\Revolution\\' . $class; if (class_exists($class)) { if (!class_exists($fqn)) { class_alias($class, $fqn); } $this->deprecated('3.0', "Replace references to class {$fqn} with {$class} to take advantage of PSR-4 autoloading.", $fqn); return $class; } } return parent::loadClass($fqn, $path, $ignorePkg, $transient); } /** * Loads a specified Context. * * Merges any context settings with the modX::$config, and performs any * other context specific initialization tasks. * * @access protected * @param string $contextKey A context identifier. * @param boolean $regenerate If true, force regeneration of the context even if already initialized. * @param array $options Array of options to override context settings. * @return boolean True if the context was properly initialized. */ protected function _initContext($contextKey, $regenerate = false, $options = null) { $initialized= false; $oldContext = is_object($this->context) ? $this->context->get('key') : ''; if (isset($this->contexts[$contextKey]) && $this->contexts[$contextKey] instanceof modContext) { $this->context= & $this->contexts[$contextKey]; } else { $this->context= $this->newObject(modContext::class); $this->context->_fields['key']= $contextKey; if (!$this->context->validate()) { $this->log(modX::LOG_LEVEL_ERROR, 'No valid context specified: ' . $contextKey); $this->context = null; } } if ($this->context) { if (!$this->context->prepare((bool) $regenerate, is_array($options) ? $options : [])) { $this->context= null; $this->log(modX::LOG_LEVEL_ERROR, 'Could not prepare context: ' . $contextKey); } else { //This fixes error with multiple contexts $this->contexts[$contextKey]=$this->context; if ($this->context->checkPolicy('load')) { $this->aliasMap= & $this->context->aliasMap; $this->resourceMap= & $this->context->resourceMap; $this->eventMap= & $this->context->eventMap; $this->pluginCache= & $this->context->pluginCache; $this->config= array_merge($this->_systemConfig, $this->context->config); $iniTZ = ini_get('date.timezone'); $cfgTZ = $this->getOption('date_timezone', $options, ''); if (!empty($cfgTZ)) { if (empty($iniTZ) || $iniTZ !== $cfgTZ) { date_default_timezone_set($cfgTZ); } } elseif (empty($iniTZ)) { date_default_timezone_set('UTC'); } if ($this->_initialized) { $this->user = null; $this->getUser(); } $initialized = true; } elseif (isset($this->contexts[$oldContext])) { $this->context =& $this->contexts[$oldContext]; } else { $this->log(modX::LOG_LEVEL_ERROR, 'Could not load context: ' . $contextKey); } } } if ($initialized) { $this->setLogLevel($this->getOption('log_level', $options, xPDO::LOG_LEVEL_ERROR)); $logTarget = $this->getOption('log_target', $options, 'FILE', true); if ($logTarget === 'FILE') { $options = []; $filename = $this->getOption('error_log_filename', $options, ''); if (!empty($filename)) $options['filename'] = $filename; $filepath = $this->getOption('error_log_filepath', $options, ''); if (!empty($filepath)) $options['filepath'] = rtrim($filepath, '/') . '/'; $this->setLogTarget( [ 'target' => 'FILE', 'options' => $options, ] ); } else { $this->setLogTarget($logTarget); } $debug = $this->getOption('debug'); if (!is_null($debug) && $debug !== '') { $this->setDebug($debug); } $this->invokeEvent( 'OnContextInit', [ 'contextKey' => $contextKey, 'options' => $options, ] ); } return $initialized; } /** * Initializes the culture settings. * * @param array|null $options Options for the culture initialization process. */ protected function _initCulture($options = null) { $defaultCultureKey = $this->getOption('cultureKey', $options, 'en'); $cultureKey = $defaultCultureKey; if (!empty($_SESSION['cultureKey'])) $cultureKey = $_SESSION['cultureKey']; if (!empty($_REQUEST['cultureKey'])) $cultureKey = $_REQUEST['cultureKey']; $cultureKey = self::sanitizeCultureKey($cultureKey, $defaultCultureKey); $this->cultureKey = $cultureKey; $this->setOption('cultureKey', $cultureKey); if ($this->getOption('setlocale', $options, true)) { $locale = setlocale(LC_ALL, '0'); $targetLocale = $this->getOption('locale', null, $locale, true); $result = setlocale(LC_ALL, $targetLocale); if ($result === false) { $this->log(modX::LOG_LEVEL_ERROR, 'Could not set the locale. Please check if the locale ' . $this->getOption('locale', null, $locale) . ' exists on your system'); } } $this->services->add('lexicon', new modLexicon($this)); $this->lexicon = $this->services->get('lexicon'); $this->invokeEvent('OnInitCulture'); } /** * Loads the error handler for this instance. * * @param array|null $options An array of options for the errorHandler. */ protected function _initErrorHandler($options = null) { if (!$this->services->has('errorHandler') && $this->errorHandler !== null && is_object($this->errorHandler)) { $this->services->add('errorHandler', $this->errorHandler); } if (!$this->services->has('errorHandler')) { if ($ehClass = $this->getOption('error_handler_class', $options, modErrorHandler::class, true)) { $ehPath = $this->getOption('error_handler_path', $options, '', true); if ($ehClass = $this->loadClass($ehClass, $ehPath, false, true)) { $errorHandler = new $ehClass($this); } } if (!$ehClass || !$errorHandler) { $errorHandler = new modErrorHandler($this); } $this->services->add('errorHandler', $errorHandler); } try { $services = $this->services; $this->errorHandler = $services->get('errorHandler'); $setErrorHandler = function($errno, $errstr, $errfile = null, $errline = null, $errcontext = null) use ($services) { return $services->get('errorHandler')->handleError($errno, $errstr, $errfile, $errline, $errcontext); }; $result = set_error_handler($setErrorHandler, $this->getOption('error_handler_types', $options, error_reporting(), true)); if ($result === false) { $this->log(modX::LOG_LEVEL_ERROR, 'Could not set error handler. Make sure your class has a function called handleError(). Result: ' . print_r($result, true)); } } catch (\Exception $exception) { $this->log(modX::LOG_LEVEL_ERROR, 'Error handler not found: ' . $exception->getMessage()); } } protected function _initHttpClient() { if (!$this->services->has(ClientInterface::class)) { // Http Client is created as a factory, so that repeat calls get a fresh client. This is done to make sure // mutable clients (perhaps they allow setting options after instantiation) do not cause side-effects elsewhere $this->services->add(ClientInterface::class, $this->services->factory(function() { $opts = $this->buildHttpClientOptions(); return new Client($opts); })); } if (!$this->services->has(ServerRequestFactoryInterface::class)) { $this->services->add(ServerRequestFactoryInterface::class, function() { return new HttpFactory(); }); } if (!$this->services->has(RequestFactoryInterface::class)) { $this->services->add(RequestFactoryInterface::class, function() { return new HttpFactory(); }); } if (!$this->services->has(ResponseFactoryInterface::class)) { $this->services->add(ResponseFactoryInterface::class, function() { return new HttpFactory(); }); } if (!$this->services->has(StreamFactoryInterface::class)) { $this->services->add(StreamFactoryInterface::class, function() { return new HttpFactory(); }); } if (!$this->services->has(UploadedFileFactoryInterface::class)) { $this->services->add(UploadedFileFactoryInterface::class, function() { return new HttpFactory(); }); } if (!$this->services->has(UriFactoryInterface::class)) { $this->services->add(UriFactoryInterface::class, function() { return new HttpFactory(); }); } } /** * Build options for the HTTP client based on configuration settings. * * @return array The HTTP client options. */ private function buildHttpClientOptions() { $opts = []; $proxyHost = $this->getOption('proxy_host', null, ''); if (!empty($proxyHost)) { $proxy_str = $proxyHost; $proxyPort = $this->getOption('proxy_port', null, ''); if (!empty($proxyPort)) { $proxy_str .= ':' . $proxyPort; } $proxyUsername = $this->getOption('proxy_username', null, ''); if (!empty($proxyUsername)) { $proxyPassword = $this->getOption('proxy_password', null, ''); $proxy_str = $proxyUsername . ':' . $proxyPassword . '@' . $proxy_str; } $opts['proxy'] = $proxy_str; } return $opts; } /** * Populates the map of events and registered plugins for each. * * @param string $contextKey Context identifier. */ protected function _initEventMap($contextKey) { if ($this->eventMap === null) { $this->eventMap= $this->getEventMap($contextKey); } } /** * Configures the session and starts it when one is wanted for this request. * * When a session is not started here (see anonymous_sessions), it can be started on * demand with modX::startSession(), e.g. when a user logs in. * * @param array|null $options Options to override Settings explicitly. */ protected function _initSession($options = null) { $contextKey= $this->context instanceof modContext ? $this->context->get('key') : null; $started = false; if ($this->configureSession($options) && $this->shouldStartSession($options)) { $started = $this->startSession(); if (!$started) { $this->log(modX::LOG_LEVEL_ERROR, 'Unable to initialize a session', '', __METHOD__, __FILE__, __LINE__); } } $this->getUser($contextKey); if ($started) { $this->refreshSessionCookie($contextKey); } } /** * Registers the session handler and applies the session and cookie settings, without starting a session. * * @param array|null $options Options to override Settings explicitly. * @return bool True if modX configured the session and is responsible for starting it; false if * sessions are disabled for the context, unavailable, or managed externally. */ protected function configureSession($options = null) { if (!$this->getOption('session_enabled', $options, true) && !isset($_GET['preview'])) { return false; } if ($this->getSessionState() !== modX::SESSION_STATE_UNINITIALIZED) { return false; } $sessionHandlerClass = $this->getOption('session_handler_class', $options); if (is_string($sessionHandlerClass) && !empty($sessionHandlerClass) && class_exists($sessionHandlerClass)) { $sh = new $sessionHandlerClass($this); if ($sh instanceof \SessionHandlerInterface) { $this->services->add('session_handler', $sh); session_set_save_handler($sh); } } if (!$this->services->has('session_handler')) { $sessionSavePath = $this->getOption('session_save_path', $options); if ($sessionSavePath && is_writable($sessionSavePath)) { session_save_path($sessionSavePath); } } $cookiePath = $this->getOption('session_cookie_path', $options, MODX_BASE_URL); if (empty($cookiePath)) { $cookiePath = $this->getOption('base_url', $options, MODX_BASE_URL); } $cookieSamesite = $this->getOption('session_cookie_samesite', $options, ''); $cookieLifetime = (int) $this->getOption('session_cookie_lifetime', $options, 0); $gcMaxlifetime = (int) $this->getOption('session_gc_maxlifetime', $options, $cookieLifetime); if ($gcMaxlifetime > 0) { ini_set('session.gc_maxlifetime', $gcMaxlifetime); } $site_sessionname = $this->getOption('session_name', $options, ''); if (!empty($site_sessionname)) { session_name($site_sessionname); } $cookie_params = [ 'lifetime' => $cookieLifetime, 'path' => $cookiePath, 'domain' => $this->getOption('session_cookie_domain', $options, ''), 'secure' => (bool) $this->getOption('session_cookie_secure', $options, false), 'httponly' => (bool) $this->getOption('session_cookie_httponly', $options, true) ]; if ($cookieSamesite !== '') { $cookie_params['samesite'] = $cookieSamesite; } session_set_cookie_params($cookie_params); return true; } /** * Determines if a session should be started while initializing modX. * * @param array|null $options Options to override Settings explicitly. * @return bool True if anonymous sessions are enabled or the client presented a session cookie. */ protected function shouldStartSession($options = null) { return $this->getOption('anonymous_sessions', $options, true) || isset($_COOKIE[session_name()]); } /** * Re-issues the session cookie when a login stored a specific lifetime for the context. * * @param string|null $contextKey The key of the context being initialized. */ protected function refreshSessionCookie($contextKey) { if (!isset($_SESSION['modx.' . $contextKey . '.session.cookie.lifetime'])) { return; } $cookieParams = session_get_cookie_params(); $sessionCookieLifetime = (int)$_SESSION['modx.' . $contextKey . '.session.cookie.lifetime']; if ($sessionCookieLifetime !== (int)$cookieParams['lifetime']) { $cookie_settings = [ 'expires' => $sessionCookieLifetime ? time() + $sessionCookieLifetime : 0, 'path' => $cookieParams['path'], 'domain' => $cookieParams['domain'], 'secure' => $cookieParams['secure'], 'httponly' => $cookieParams['httponly'] ]; if (!empty($cookieParams['samesite'])) { $cookie_settings['samesite'] = $cookieParams['samesite']; } setcookie(session_name(), session_id(), $cookie_settings); } } /** * Loads the modX system configuration settings. * * @access protected * @return boolean True if successful. */ protected function _loadConfig() { $this->config = $this->_config; $this->getCacheManager(); $config = $this->cacheManager->get('config', [ xPDO::OPT_CACHE_KEY => $this->getOption('cache_system_settings_key', null, 'system_settings'), xPDO::OPT_CACHE_HANDLER => $this->getOption('cache_system_settings_handler', null, $this->getOption(xPDO::OPT_CACHE_HANDLER)), xPDO::OPT_CACHE_FORMAT => (int) $this->getOption('cache_system_settings_format', null, $this->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)), ] ); if (empty($config)) { $config = $this->cacheManager->generateConfig(); } if (empty($config)) { $config = []; if (!$settings = $this->getCollection(modSystemSetting::class)) { return false; } /** @var modSystemSetting $setting */ foreach ($settings as $setting) { $config[$setting->get('key')]= $setting->get('value'); } } $this->config = array_merge($this->config, $config); $this->_systemConfig = $this->config; return true; } /** * Provides modX the ability to use modRegister instances as log targets, * and dispatches to a PSR-3 logger when one is set via {@see setLogger()}. * * When a PSR-3 logger is set it replaces legacy FILE/ECHO/HTML output. * modRegister logging is always preserved alongside the PSR-3 logger. * * {@inheritdoc} */ protected function _log($level, $msg, $target= '', $def= '', $file= '', $line= '') { if ($level !== xPDO::LOG_LEVEL_FATAL && $level > $this->logLevel && $this->_debug !== true) { return; } if (empty($target)) { $target = $this->logTarget; } $targetOptions = []; $targetObj = $target; if (is_array($target)) { if (isset($target['options'])) $targetOptions = $target['options']; $targetObj = isset($target['target']) ? $target['target'] : 'ECHO'; } $hasPsrLogger = $this->logger instanceof LoggerInterface && !($this->logger instanceof xPDOLogger); // Dispatch to PSR-3 logger (no backtrace — file/line only if caller provided them) if ($hasPsrLogger) { $this->_dispatchToPsrLogger($level, $msg, $def, $file, $line); } // modRegister target if (is_object($targetObj) && $targetObj instanceof modRegister) { if (empty($file)) { $file = $_SERVER['PHP_SELF'] ?? ($_SERVER['SCRIPT_FILENAME'] ?? ''); } if ($level === modX::LOG_LEVEL_FATAL) { $this->_logInRegister($targetObj, $level, $msg, $def, $file, $line); $this->sendError('fatal'); } if ($this->_debug === true || $level <= $this->logLevel) { $this->_logInRegister($targetObj, $level, $msg, $def, $file, $line); } return; } // PSR-3 logger replaces legacy FILE/ECHO/HTML output if ($hasPsrLogger) { if ($level === modX::LOG_LEVEL_FATAL) { $this->sendError('fatal'); } return; } // Legacy path (no PSR-3 logger) if ($level === modX::LOG_LEVEL_FATAL) { while (ob_get_level() && @ob_end_clean()) {} if ($targetObj == 'FILE' && $cacheManager = $this->getCacheManager()) { $filename = isset($targetOptions['filename']) ? $targetOptions['filename'] : 'error.log'; $filepath = isset($targetOptions['filepath']) ? $targetOptions['filepath'] : $this->getCachePath() . xPDOCacheManager::LOG_DIR; $cacheManager->writeFile($filepath . $filename, '[' . date('Y-m-d H:i:s') . '] (' . $this->_getLogLevel($level) . $def . $file . $line . ') ' . $msg . "\n" . ($this->getDebug() === true ? '
' . "\n" . print_r(debug_backtrace(), true) . "\n" . '
' : ''), 'a'); } $this->sendError('fatal'); } $logger = $this->logger; $this->logger = null; try { parent::_log($level, $msg, $target, $def, $file, $line); } finally { $this->logger = $logger; } } /** * Provides custom logging functionality for modRegister targets. * * @access protected * @param modRegister $register The modRegister instance to send to * @param int $level The level of error or message that occurred * @param string $msg The message to send to the register * @param string $def The type of error that occurred * @param string $file The filename of the file that the message occurs for * @param string $line The line number of the file that the message occurs for */ protected function _logInRegister($register, $level, $msg, $def, $file, $line) { $timestamp = date('Y-m-d H:i:s'); $messageKey = (string) time(); $messageKey .= '-' . sprintf("%06d", $this->_logSequence); $message = [ 'timestamp' => $timestamp, 'level' => $this->_getLogLevel($level), 'msg' => $msg, 'def' => $def, 'file' => $file, 'line' => $line, ]; $options = []; if ($level === xPDO::LOG_LEVEL_FATAL) { $options['kill'] = true; } $register->send('', [$messageKey => $message], $options); $this->_logSequence++; } /** * Dispatch a log message to the PSR-3 logger. * * Maps xPDO log levels to PSR-3 levels and builds a context array. * File and line are passed as-is (no backtrace resolution); Monolog users * can add IntrospectionProcessor for automatic caller info. * * @param int $level The xPDO log level constant. * @param mixed $msg The log message. * @param string $def The defining structure (class/method). * @param string $file The source filename, if provided by the caller. * @param string $line The source line number, if provided by the caller. */ private function _dispatchToPsrLogger($level, $msg, $def, $file, $line): void { if (is_string($msg)) { $message = $msg; } elseif (is_object($msg) && method_exists($msg, '__toString')) { $message = (string) $msg; } else { $message = print_r($msg, true); } $context = [ 'def' => $def, 'file' => $file, 'line' => $line, 'xpdo_level' => $level, ]; switch ($level) { case xPDO::LOG_LEVEL_DEBUG: $psrLevel = LogLevel::DEBUG; break; case xPDO::LOG_LEVEL_INFO: $psrLevel = LogLevel::INFO; break; case xPDO::LOG_LEVEL_WARN: $psrLevel = LogLevel::WARNING; break; case xPDO::LOG_LEVEL_ERROR: $psrLevel = LogLevel::ERROR; break; case xPDO::LOG_LEVEL_FATAL: $psrLevel = LogLevel::CRITICAL; break; default: $psrLevel = LogLevel::NOTICE; } $this->logger->log($psrLevel, $message, $context); } /** * Executed after the response is sent and execution is completed. * * @access protected */ public function _postProcess() { if ($this->resourceGenerated && $this->getOption('cache_resource', null, true)) { if (is_object($this->resource) && $this->resource instanceof modResource && $this->resource->get('id') && $this->resource->get('cacheable')) { $this->resource->_contextKey = $this->context->get('key'); $this->invokeEvent('OnBeforeSaveWebPageCache'); $this->cacheManager->generateResource($this->resource); } } $this->invokeEvent('OnWebPageComplete'); } /** * Determine whether the passed value is a unix timestamp and, optionally, whether it is within * a specified range * @param string|int $value The timestamp to test * @param bool $limitStart Optional minimum timestamp value that will validate * @param bool $limitEnd Optional maximum timestamp value that will validate * @return bool */ public static function isValidTimestamp($value, $limitStart = false, $limitEnd = false): bool { // Check that value represents an integer (signed or unsigned) if (!preg_match('/^-?[1-9][0-9]*$/', $value)) { return false; } $value = (int)$value; if (!($value <= PHP_INT_MAX && $value >= ~PHP_INT_MAX)) { return false; } if (($limitStart !== false && $value < (int)$limitStart) || ($limitEnd !== false && $value > (int)$limitEnd)) { return false; } return true; } }