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 "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, '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,'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), "